<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Posts on Alex Jacobs</title>
    <link>https://alex-jacobs.com/posts/</link>
    <description>Recent content in Posts on Alex Jacobs</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Sun, 04 Jan 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://alex-jacobs.com/posts/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Beating BERT? Small LLMs vs Fine-Tuned Encoders for Classification</title>
      <link>https://alex-jacobs.com/posts/beatingbert/</link>
      <pubDate>Sun, 04 Jan 2026 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/beatingbert/</guid>
      <description>I ran 32 experiments comparing small LLMs to BERT on classification tasks. Turns out 2018-era BERT is still really good at what it does.</description>
      <content:encoded><![CDATA[<p>&ldquo;Just use an LLM.&rdquo;</p>
<p>That was my advice to a colleague recently when they asked about a classification problem. Who fine-tunes BERT anymore? Haven&rsquo;t decoder models eaten the entire NLP landscape?</p>
<p>The look I got back was&hellip; skeptical. And it stuck with me.</p>
<p>I&rsquo;ve been deep in LLM-land for a few years now. When your daily driver can architect systems, write production code, and reason through problems better than most junior devs, you start reaching for it reflexively. Maybe my traditional ML instincts had atrophied.</p>
<p>So I decided to actually test my assumptions instead of just vibing on them.</p>
<p>I ran 32 experiments pitting small instruction-tuned LLMs against good old BERT and DeBERTa. I figured I&rsquo;d just be confirming what I already believed, that these new decoder models would obviously crush the ancient encoders.</p>
<p>I was wrong.</p>
<p>The results across Gemma 2B, Qwen 0.5B/1.5B, BERT-base, and DeBERTa-v3 were&hellip; not what I expected. If you&rsquo;re trying to decide between these approaches for classification, you might want to actually measure things instead of assuming the newer model is better.</p>
<p><img loading="lazy" src="/posts/beatingbert/scorecard.png" type="" alt="TL;DR: DeBERTa wins 3/4 tasks, LLM wins on adversarial NLI, but LLMs need zero training"  /></p>
<p>All the code is <a href="https://github.com/alexjacobs08/beatingBERT">on GitHub</a> if you want to run your own experiments.</p>
<h2 id="experiment-setup">Experiment Setup</h2>
<h3 id="what-i-tested">What I Tested</h3>
<p><strong>BERT Family (Fine-tuned)</strong></p>
<ul>
<li>BERT-base-uncased (110M parameters)</li>
<li>DeBERTa-v3-base (184M parameters)</li>
</ul>
<p><strong>Small LLMs</strong></p>
<ul>
<li>Qwen2-0.5B-Instruct</li>
<li>Qwen2.5-1.5B-Instruct</li>
<li>Gemma-2-2B-it</li>
</ul>
<p>For the LLMs, I tried two approaches:</p>
<ol>
<li><strong>Zero-shot</strong> - Just prompt engineering, no training</li>
<li><strong>Few-shot (k=5)</strong> - Include 5 examples in the prompt</li>
</ol>
<h3 id="tasks">Tasks</h3>
<p>Four classification benchmarks ranging from easy sentiment to adversarial NLI:</p>
<table>
<thead>
<tr>
<th>Task</th>
<th>Type</th>
<th>Labels</th>
<th>Difficulty</th>
</tr>
</thead>
<tbody>
<tr>
<td>SST-2</td>
<td>Sentiment</td>
<td>2</td>
<td>Easy</td>
</tr>
<tr>
<td>RTE</td>
<td>Textual Entailment</td>
<td>2</td>
<td>Medium</td>
</tr>
<tr>
<td>BoolQ</td>
<td>Yes/No QA</td>
<td>2</td>
<td>Medium</td>
</tr>
<tr>
<td>ANLI (R1)</td>
<td>Adversarial NLI</td>
<td>3</td>
<td>Hard</td>
</tr>
</tbody>
</table>
<h3 id="methodology">Methodology</h3>
<p>For anyone who wants to reproduce this or understand what &ldquo;fine-tuned&rdquo; and &ldquo;zero-shot&rdquo; actually mean here:</p>
<p><strong>BERT/DeBERTa Fine-tuning:</strong></p>
<ul>
<li>Standard HuggingFace Trainer with AdamW optimizer</li>
<li>Learning rate: 2e-5, batch size: 32, epochs: 3</li>
<li>Max sequence length: 128 tokens</li>
<li>Evaluation on validation split (GLUE test sets don&rsquo;t have public labels)</li>
</ul>
<p><strong>LLM Zero-shot:</strong></p>
<ul>
<li>Greedy decoding (temperature=0.0) for deterministic outputs</li>
<li>Task-specific prompts asking for single-word classification labels</li>
<li>No examples in context—just instructions and the input text</li>
</ul>
<p><strong>LLM Few-shot (k=5):</strong></p>
<ul>
<li>Same as zero-shot, but with 5 labeled examples prepended to each prompt</li>
<li>Examples randomly sampled from training set (stratified by class)</li>
</ul>
<p>All experiments used a fixed random seed (99) for reproducibility. Evaluation metrics are accuracy on the validation split. Hardware: RunPod instance with RTX A4500 (20GB VRAM), 20GB RAM, 5 vCPU.</p>
<p><img loading="lazy" src="/posts/beatingbert/nvitop.png" type="" alt="nvitop running on RunPod"  />
<em>I&rsquo;d forgotten how pretty text-only land can be. When you spend most of your time in IDEs and notebooks, SSH-ing into a headless GPU box and watching nvitop do its thing feels almost meditative.</em></p>
<h2 id="results">Results</h2>
<p>Let&rsquo;s dive into what actually happened:</p>


<style>
.results-table { width: 100%; border-collapse: collapse; margin: 1.5rem 0; font-size: 0.9rem; }
.results-table th, .results-table td { padding: 0.5rem 0.75rem; text-align: left; border-bottom: 1px solid var(--border); }
.results-table th { color: var(--secondary); font-weight: 500; }
.results-table td { color: var(--content); }
.winner { background: rgba(87, 62, 170, 0.15); font-weight: 600; border-radius: 4px; padding: 0.2rem 0.4rem; }
</style>
<table class="results-table">
<thead>
<tr><th>Model</th><th>Method</th><th>SST-2</th><th>RTE</th><th>BoolQ</th><th>ANLI</th></tr>
</thead>
<tbody>
<tr><td><strong>DeBERTa-v3</strong></td><td>Fine-tuned</td><td><span class="winner">94.8%</span></td><td><span class="winner">80.9%</span></td><td><span class="winner">82.6%</span></td><td>47.4%</td></tr>
<tr><td><strong>BERT-base</strong></td><td>Fine-tuned</td><td>91.5%</td><td>61.0%</td><td>71.5%</td><td>35.3%</td></tr>
<tr><td>Qwen2.5-1.5B</td><td>Zero-shot</td><td>93.8%</td><td>78.7%</td><td>74.6%</td><td>40.8%</td></tr>
<tr><td>Qwen2.5-1.5B</td><td>Few-shot</td><td>89.0%</td><td>53.4%</td><td>73.6%</td><td>45.0%</td></tr>
<tr><td>Gemma-2-2B</td><td>Zero-shot</td><td>90.0%</td><td>61.4%</td><td>80.9%</td><td>36.1%</td></tr>
<tr><td>Gemma-2-2B</td><td>Few-shot</td><td>86.5%</td><td>73.6%</td><td>81.5%</td><td><span class="winner">47.8%</span></td></tr>
<tr><td>Qwen2-0.5B</td><td>Zero-shot</td><td>87.6%</td><td>53.1%</td><td>61.8%</td><td>33.2%</td></tr>
</tbody>
</table>

<p><img loading="lazy" src="/posts/beatingbert/accuracy_comparison.png" type="" alt="Model Accuracy Comparison"  /></p>
<p><strong>DeBERTa-v3 wins most tasks—but not all</strong></p>
<p>DeBERTa hit 94.8% on SST-2, 80.9% on RTE, and 82.6% on BoolQ. For standard classification with decent training data, the fine-tuned encoders still dominate.</p>
<p>On ANLI—the hardest benchmark, specifically designed to fool models—Gemma few-shot actually beats DeBERTa (47.8% vs 47.4%). It&rsquo;s a narrow win, but it&rsquo;s a win on the task that matters most for robustness.</p>
<p><strong>Zero-shot LLMs actually beat BERT-base</strong></p>
<p>The LLMs aren&rsquo;t losing to BERT—they&rsquo;re losing to DeBERTa. Qwen2.5-1.5B zero-shot hit 93.8% on SST-2, beating BERT-base&rsquo;s 91.5%. Same story on RTE (78.7% vs 61.0%) and BoolQ (Gemma&rsquo;s 80.9% vs BERT&rsquo;s 71.5%). For models running purely on prompts with zero training? I&rsquo;m calling it a win.</p>
<p><strong>Few-shot is a mixed bag</strong></p>
<p>Adding examples to the prompt doesn&rsquo;t always help.</p>
<p>On RTE, Qwen2.5-1.5B went from 78.7% zero-shot down to 53.4% with few-shot. On SST-2, it dropped from 93.8% to 89.0%. But on ANLI, few-shot helped significantly—Gemma jumped from 36.1% to 47.8%, enough to beat DeBERTa.</p>
<p>Few-shot helps on harder tasks where examples demonstrate the thought process, but can confuse models on simpler pattern matching tasks where they already &ldquo;get it.&rdquo; Sometimes examples add noise instead of signal.</p>
<h2 id="bert-goes-brrrr">BERT Goes Brrrr</h2>
<p>Okay, so the accuracy gap isn&rsquo;t huge. Maybe I could still justify using an LLM?</p>
<p>Then I looked at throughput:</p>
<table>
<thead>
<tr>
<th>Model</th>
<th>Method</th>
<th>Throughput (samples/s)</th>
<th>Latency (ms/sample)</th>
</tr>
</thead>
<tbody>
<tr>
<td>BERT-base</td>
<td>Fine-tuned</td>
<td><strong>277</strong></td>
<td>3.6</td>
</tr>
<tr>
<td>DeBERTa-v3</td>
<td>Fine-tuned</td>
<td><strong>232</strong></td>
<td>4.3</td>
</tr>
<tr>
<td>Qwen2-0.5B</td>
<td>Zero-shot</td>
<td>17.5</td>
<td>57</td>
</tr>
<tr>
<td>Qwen2.5-1.5B</td>
<td>Zero-shot</td>
<td>12.3</td>
<td>81</td>
</tr>
<tr>
<td>Gemma-2-2B</td>
<td>Zero-shot</td>
<td>11.6</td>
<td>86</td>
</tr>
</tbody>
</table>
<p><img loading="lazy" src="/posts/beatingbert/accuracy_vs_latency.png" type="" alt="Accuracy vs Latency"  /></p>
<p><strong>BERT is ~20x faster.</strong></p>
<p>BERT processes 277 samples per second. Gemma-2-2B manages 12. If you&rsquo;re classifying a million documents, that&rsquo;s one hour vs a full day.</p>
<p>Encoders process the whole sequence in one forward pass. Decoders generate tokens autoregressively, even just to output &ldquo;positive&rdquo; or &ldquo;negative&rdquo;.</p>
<blockquote>
<p><strong>Note on LLM latency:</strong> These numbers use <code>max_length=256</code> for tokenization. When I bumped it to <code>max_length=2048</code>, latency jumped 8x—from 57ms to 445ms per sample for Qwen-0.5B. Context window scales roughly linearly with inference time. For short classification tasks, keep it short or make it dynamic.</p>
</blockquote>
<h3 id="try-it-yourself">Try It Yourself</h3>
<p>These models struggled on nuanced reviews. Can you do better? Try classifying some of the trickiest examples from my experiments:</p>


<style>
.classifier-demo {
  max-width: 100%;
  margin: 1.5rem 0;
  padding: 1.5rem;
  border: 1px solid var(--border);
  border-radius: var(--radius);
  background: var(--code-bg);
}
.demo-header {
  text-align: center;
  margin-bottom: 1rem;
}
.demo-title {
  margin: 0 0 0.25rem 0;
  font-size: 1.1rem;
  color: var(--primary);
}
.demo-subtitle {
  margin: 0 0 0.75rem 0;
  font-size: 0.85rem;
  color: var(--secondary);
}
.demo-score {
  display: flex;
  justify-content: center;
  gap: 2rem;
  font-size: 0.9rem;
  color: var(--secondary);
}
.demo-score strong {
  color: var(--primary);
}
.review-box {
  background: var(--entry);
  padding: 1rem 1.25rem;
  border-radius: var(--radius);
  margin: 1rem 0;
  font-style: italic;
  line-height: 1.6;
  border-left: 3px solid #573eaa;
  color: var(--content);
}
.btn-group {
  display: flex;
  gap: 0.75rem;
  justify-content: center;
  margin: 1rem 0;
}
.demo-btn {
  padding: 0.6rem 1.5rem;
  font-size: 0.9rem;
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  transition: all 0.2s;
  font-weight: 500;
}
.demo-btn:hover:not(:disabled) { opacity: 0.9; }
.demo-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-positive { background: #27ae60; color: white; }
.btn-negative { background: #c0392b; color: white; }
.result-box {
  margin-top: 1rem;
  padding: 1rem;
  border-radius: var(--radius);
  display: none;
}
.result-box.show { display: block; }
.result-correct { background: rgba(39, 174, 96, 0.15); border: 1px solid rgba(39, 174, 96, 0.3); }
.result-wrong { background: rgba(192, 57, 43, 0.15); border: 1px solid rgba(192, 57, 43, 0.3); }
.model-results {
  margin-top: 0.75rem;
  font-size: 0.8rem;
  color: var(--secondary);
}
.model-row {
  display: flex;
  justify-content: space-between;
  padding: 0.2rem 0;
  border-bottom: 1px solid var(--border);
}
.model-row:last-child { border-bottom: none; }
.model-correct { color: #27ae60; }
.model-wrong { color: #c0392b; }
.next-btn {
  display: block;
  margin: 1rem auto 0;
  padding: 0.5rem 1.25rem;
  background: #573eaa;
  color: white;
  border: none;
  border-radius: var(--radius);
  cursor: pointer;
  font-size: 0.85rem;
}
.next-btn:hover { background: #6549c0; }
.progress-bar {
  height: 3px;
  background: var(--border);
  border-radius: 2px;
  margin-bottom: 1rem;
}
.progress-fill {
  height: 100%;
  background: #573eaa;
  border-radius: 2px;
  transition: width 0.3s;
}
.demo-complete {
  text-align: center;
  padding: 1rem;
}
.final-score {
  font-size: 1.25rem;
  font-weight: 600;
  margin: 0.5rem 0;
  color: var(--primary);
}
#completeSummary {
  color: var(--secondary);
}
</style>

<div class="classifier-demo" id="classifierDemo">
  <div class="demo-header">
    <div class="demo-title">Can You Beat the Models?</div>
    <p class="demo-subtitle">Classify these tricky movie reviews</p>
    <div class="demo-score">
      <span>You: <strong id="userScore">0</strong>/<span id="totalAnswered">0</span></span>
      <span>Models: <strong id="modelScore">0</strong>/<span id="totalAnswered2">0</span></span>
    </div>
  </div>
  <div class="progress-bar">
    <div class="progress-fill" id="progressFill" style="width: 0%"></div>
  </div>
  <div id="questionArea">
    <div class="review-box" id="reviewText"></div>
    <div class="btn-group">
      <button class="demo-btn btn-negative" onclick="submitAnswer('negative')">Negative</button>
      <button class="demo-btn btn-positive" onclick="submitAnswer('positive')">Positive</button>
    </div>
    <div class="result-box" id="resultBox">
      <div id="resultText"></div>
      <div class="model-results" id="modelResults"></div>
      <button class="next-btn" onclick="nextQuestion()">Next Review →</button>
    </div>
  </div>
  <div id="completeArea" style="display:none;" class="demo-complete">
    <div class="demo-title">Challenge Complete!</div>
    <div class="final-score">You: <span id="finalUserScore"></span> | Models: <span id="finalModelScore"></span></div>
    <p id="completeSummary"></p>
    <button class="next-btn" onclick="restartDemo()">Play Again</button>
  </div>
</div>

<script>
const demoExamples = [
  {text: "hilariously inept and ridiculous.", true_label: "positive", predictions: {"Gemma": "negative", "Qwen": "negative"}},
  {text: "all that's missing is the spontaneity, originality and delight.", true_label: "negative", predictions: {"Gemma": "positive", "Qwen": "positive"}},
  {text: "reign of fire looks as if it was made without much thought -- and is best watched that way.", true_label: "positive", predictions: {"Gemma": "negative", "Qwen": "negative"}},
  {text: "we root for (clara and paul), even like them, though perhaps it's an emotion closer to pity.", true_label: "positive", predictions: {"Gemma": "negative", "Qwen": "negative"}},
  {text: "a solid film... but more conscientious than it is truly stirring.", true_label: "positive", predictions: {"Gemma": "negative", "Qwen": "positive"}},
  {text: "this riveting world war ii moral suspense story deals with the shadow side of american culture: racial prejudice in its ugly and diverse forms.", true_label: "negative", predictions: {"Gemma": "positive", "Qwen": "positive"}}
];
let currentIndex = 0, userCorrect = 0, modelCorrect = 0, totalAnswered = 0;
function initDemo() {
  currentIndex = 0; userCorrect = 0; modelCorrect = 0; totalAnswered = 0;
  document.getElementById('completeArea').style.display = 'none';
  document.getElementById('questionArea').style.display = 'block';
  updateScores(); showQuestion();
}
function showQuestion() {
  const ex = demoExamples[currentIndex];
  document.getElementById('reviewText').textContent = '"' + ex.text + '"';
  document.getElementById('resultBox').classList.remove('show');
  document.querySelectorAll('.demo-btn').forEach(b => b.disabled = false);
  document.getElementById('progressFill').style.width = ((currentIndex / demoExamples.length) * 100) + '%';
}
function submitAnswer(answer) {
  const ex = demoExamples[currentIndex];
  const correct = answer === ex.true_label;
  totalAnswered++;
  if (correct) userCorrect++;
  let mc = 0;
  Object.values(ex.predictions).forEach(p => { if (p === ex.true_label) mc++; });
  modelCorrect += mc / Object.keys(ex.predictions).length;
  const resultBox = document.getElementById('resultBox');
  resultBox.className = 'result-box show ' + (correct ? 'result-correct' : 'result-wrong');
  document.getElementById('resultText').innerHTML = correct
    ? '<strong>Correct!</strong> This review is ' + ex.true_label + '.'
    : '<strong>Tricky!</strong> This review is actually <em>' + ex.true_label + '</em>.';
  let modelHtml = '<strong>Model predictions:</strong>';
  for (const [model, pred] of Object.entries(ex.predictions)) {
    const isCorrect = pred === ex.true_label;
    modelHtml += '<div class="model-row"><span>' + model + '</span><span class="' + (isCorrect ? 'model-correct' : 'model-wrong') + '">' + pred + ' ' + (isCorrect ? '✓' : '✗') + '</span></div>';
  }
  document.getElementById('modelResults').innerHTML = modelHtml;
  document.querySelectorAll('.demo-btn').forEach(b => b.disabled = true);
  updateScores();
}
function updateScores() {
  document.getElementById('userScore').textContent = userCorrect;
  document.getElementById('modelScore').textContent = modelCorrect.toFixed(1);
  document.getElementById('totalAnswered').textContent = totalAnswered;
  document.getElementById('totalAnswered2').textContent = totalAnswered;
}
function nextQuestion() {
  currentIndex++;
  if (currentIndex >= demoExamples.length) showComplete();
  else showQuestion();
}
function showComplete() {
  document.getElementById('questionArea').style.display = 'none';
  document.getElementById('completeArea').style.display = 'block';
  document.getElementById('finalUserScore').textContent = userCorrect + '/' + demoExamples.length;
  document.getElementById('finalModelScore').textContent = modelCorrect.toFixed(1) + '/' + demoExamples.length;
  const diff = userCorrect - modelCorrect;
  let msg = diff > 1 ? "You crushed the AI! Human intuition wins." : diff > 0 ? "You edged out the models!" : diff === 0 ? "Dead heat with AI." : "The models got you this time. These are genuinely tricky!";
  document.getElementById('completeSummary').textContent = msg;
}
function restartDemo() { initDemo(); }
document.addEventListener('DOMContentLoaded', initDemo);
if (document.readyState !== 'loading') initDemo();
</script>

<h2 id="when-llms-make-sense">When LLMs Make Sense</h2>
<p>Despite the efficiency gap, there are cases where small LLMs are the right choice:</p>
<p><strong>Zero Training Data</strong></p>
<p>If you have no labeled data, LLMs win by default. Zero-shot Qwen2.5-1.5B at 93.8% on SST-2 is production-ready without a single training example. You can&rsquo;t fine-tune BERT with zero examples.</p>
<p><strong>Rapidly Changing Categories</strong></p>
<p>If your categories change frequently (new product types, emerging topics), re-prompting an LLM takes seconds. Re-training BERT requires new labeled data, training time, validation, deployment. The iteration cycle matters.</p>
<p><strong>Explanations with Predictions</strong></p>
<p>LLMs can provide reasoning: &ldquo;This review is negative because the customer mentions &lsquo;defective product&rsquo; and &lsquo;waste of money.&rsquo;&rdquo; BERT gives you a probability. Sometimes you need the story, not just the number.</p>
<p><strong>Low Volume</strong></p>
<p>If you&rsquo;re processing 100 support tickets a day, throughput doesn&rsquo;t matter. The 20x speed difference is irrelevant when you&rsquo;re not hitting any resource constraints.</p>
<h2 id="when-bert-still-wins">When BERT Still Wins</h2>
<p><strong>High-Volume Production Systems</strong></p>
<p>If you&rsquo;re classifying millions of items daily, BERT&rsquo;s 20x throughput advantage matters. That&rsquo;s a job finishing in an hour vs. running all day.</p>
<p><strong>Well-Defined, Stable Tasks</strong></p>
<p>Sentiment analysis. Spam detection. Topic classification. If your task definition hasn&rsquo;t changed since 2019, fine-tuned BERT is proven and stable. No need to fix what isn&rsquo;t broken.</p>
<p><strong>You Have Training Data</strong></p>
<p>With a few thousand labeled examples, fine-tuned DeBERTa will beat small LLMs. It&rsquo;s a dedicated specialist vs. a generalist. Specialization still works.</p>
<p><strong>Latency Matters</strong></p>
<p>Real-time classification in a user-facing app where every millisecond counts? BERT&rsquo;s parallel processing wins. LLMs can&rsquo;t compete on speed.</p>
<h2 id="limitations">Limitations</h2>
<p>Before you @ me on Twitter—yes, I know this isn&rsquo;t the final word. Some caveats:</p>
<p><strong>I only tested small LLMs.</strong> Kept everything under 2B parameters to fit comfortably on a 20GB GPU. Bigger models like Llama-3-8B or Qwen-7B would probably do better, but then the efficiency comparison becomes even more lopsided. You&rsquo;re not beating BERT&rsquo;s throughput with a 7B model.</p>
<p><strong>Generic prompts.</strong> I used straightforward prompts without heavy optimization. Task-specific prompt engineering could boost LLM performance. DSPy-style optimization would probably help too—but that&rsquo;s another blog post.</p>
<p><strong>Four benchmarks isn&rsquo;t everything.</strong> There are plenty of classification scenarios I didn&rsquo;t test. Your domain might be different. Measure, don&rsquo;t assume.</p>
<h2 id="conclusion">Conclusion</h2>
<p>So, can small LLMs beat BERT at classification?</p>
<p>Sometimes, and on the hardest task, they actually do. Gemma few-shot edges out DeBERTa on adversarial NLI, the benchmark specifically designed to break models.</p>
<p>DeBERTa-v3 still wins 3 out of 4 tasks when you have training data. And BERT&rsquo;s efficiency advantage is real—~20x faster throughput matters when you&rsquo;re processing millions of documents and paying for compute.</p>
<p>Zero-shot LLMs aren&rsquo;t just a parlor trick either. Qwen2.5-1.5B hits 93.8% on sentiment with zero training examples—that&rsquo;s production-ready without a single label. For cold-start problems, rapidly changing domains, or when you need explanations alongside predictions, they genuinely work.</p>
<p>Hopefully this gives some actual data points for making that call instead of just following the hype cycle.</p>
<p>All the code is <a href="https://github.com/alexjacobs08/beatingBERT">on GitHub</a>. Go run your own experiments.</p>
<h2 id="related-reading">Related reading</h2>
<ul>
<li><a href="/posts/practicalaifeatures/">A Production Framework for LLM Feature Evaluation</a> — where classification fits among the LLM features that actually ship.</li>
<li><a href="/posts/rag/">RAG: From Context Injection to Knowledge Integration</a> — the other pattern everyone reaches for, and its architectural limits.</li>
</ul>
<hr>
<p><em>Surely I&rsquo;ve made some embarrassing mistakes here. Don&rsquo;t just tell me—tell everyone! Share this post on your favorite social media with your corrections :)</em></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>The Case Against pgvector</title>
      <link>https://alex-jacobs.com/posts/the-case-against-pgvector/</link>
      <pubDate>Wed, 29 Oct 2025 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/the-case-against-pgvector/</guid>
      <description>The real limitations of pgvector in production: index choice between IVFFlat and HNSW, why real-time search is hard, pre- vs post-filtering pain, how performance degrades at scale, and when a dedicated vector database is the better call.</description>
      <content:encoded><![CDATA[

<a class="simon-callout" href="https://simonwillison.net/2025/Nov/3/the-case-against-pgvector/" target="_blank" rel="noopener noreferrer">
  <span class="simon-label">Simon Willison</span> shared his thoughts on this post <span class="simon-arrow">&rarr;</span>
</a>

<h2 id="everyone-loves-pgvector-in-theory">Everyone Loves pgvector (in theory)</h2>
<p>If you&rsquo;ve spent any time in the vector search space over the past year, you&rsquo;ve probably read blog posts explaining why pgvector is the obvious choice for your vector database needs. The argument goes something like this: you already have Postgres, vector embeddings are just another data type, why add complexity with a dedicated vector database when you can keep everything in one place?</p>
<p>It&rsquo;s a compelling story. And like most of the AI influencer bullshit that fills my timeline, it glosses over the inconvenient details.</p>
<p>I&rsquo;m not here to tell you pgvector is bad. It&rsquo;s not. It&rsquo;s a useful extension that brings vector similarity search to Postgres. But after spending some time trying to build a production system on top of it, I&rsquo;ve learned that the gap between &ldquo;works in a demo&rdquo; and &ldquo;scales in production&rdquo; is&hellip; significant.</p>
<h2 id="nobodys-actually-run-this-in-production">Nobody&rsquo;s actually run this in production</h2>
<p>What bothers me most: the majority of content about pgvector reads like it was written by someone who spun up a local Postgres instance, inserted 10,000 vectors, ran a few queries, and called it a day. The posts are optimistic, the benchmarks are clean, and the conclusions are confident.</p>
<p>They&rsquo;re also missing about 80% of what you actually need to know.</p>
<p>I&rsquo;ve read through  <style>
    .pgvector-trigger {
        display: inline;
        color: var(--primary, #3273dc);
        text-decoration: underline;
        cursor: pointer;
        text-underline-offset: 2px;
        transition: opacity 0.2s;
    }
    
    .pgvector-trigger:hover {
        opacity: 0.7;
    }

    .pgvector-overlay {
        display: none;
        position: fixed;
        z-index: 9999;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgba(0, 0, 0, 0.7);
        animation: fadeIn 0.2s ease-in;
    }

    .pgvector-overlay.active {
        display: flex;
        align-items: center;
        justify-content: center;
        padding: 20px;
    }

    .pgvector-modal {
        background-color: var(--entry, #fff);
        color: var(--content, #000);
        margin: auto;
        padding: 32px;
        border-radius: 16px;
        width: 90%;
        max-width: 650px;
        max-height: 80vh;
        box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
        animation: slideUp 0.3s ease-out;
        display: flex;
        flex-direction: column;
        position: relative;
    }

    .pgvector-close {
        position: absolute;
        top: 16px;
        right: 16px;
        background: none;
        border: none;
        font-size: 28px;
        font-weight: 300;
        cursor: pointer;
        color: var(--secondary, #666);
        padding: 4px 8px;
        line-height: 1;
        border-radius: 4px;
        transition: all 0.2s;
    }

    .pgvector-close:hover {
        background-color: var(--code-bg, #f5f5f5);
        color: var(--content, #000);
    }

    .pgvector-content {
        overflow-y: auto;
    }

    .pgvector-posts {
        display: flex;
        flex-direction: column;
        gap: 8px;
    }

    .pgvector-post {
        padding: 0;
        list-style: none;
    }

    .pgvector-post a {
        text-decoration: none;
        color: var(--primary, #3273dc);
        display: block;
        font-size: 0.95em;
        line-height: 1.6;
        padding: 8px 0;
        transition: all 0.2s;
    }

    .pgvector-post a:hover {
        color: var(--content, #000);
        padding-left: 8px;
    }

    @keyframes fadeIn {
        from { opacity: 0; }
        to { opacity: 1; }
    }

    @keyframes slideUp {
        from {
            opacity: 0;
            transform: translateY(20px) scale(0.95);
        }
        to {
            opacity: 1;
            transform: translateY(0) scale(1);
        }
    }

    @media (max-width: 600px) {
        .pgvector-modal {
            width: 95%;
            max-height: 85vh;
            padding: 24px;
            border-radius: 12px;
        }

        .pgvector-close {
            top: 12px;
            right: 12px;
        }

        .pgvector-post a {
            font-size: 0.9em;
        }
    }
</style> <span class="pgvector-trigger" onclick="openPgvectorModal()">dozens of these posts.</span><div id="pgvectorOverlay" class="pgvector-overlay" onclick="closePgvectorModal(event)">
    <div class="pgvector-modal" onclick="event.stopPropagation()">
        <button class="pgvector-close" onclick="closePgvectorModal()">&times;</button>
        <div class="pgvector-content">
            <div class="pgvector-posts">
                <div class="pgvector-post"><a href="https://neon.com/blog/understanding-vector-search-and-hnsw-index-with-pgvector" target="_blank" rel="noopener">Understanding Vector Search and HNSW Index with pgvector</a></div>
                <div class="pgvector-post"><a href="https://www.crunchydata.com/blog/hnsw-indexes-with-postgres-and-pgvector" target="_blank" rel="noopener">HNSW Indexes with Postgres and pgvector</a></div>
                <div class="pgvector-post"><a href="https://www.stormatics.tech/blog/understand-indexes-in-pgvector" target="_blank" rel="noopener">Understand Indexes in pgvector</a></div>
                <div class="pgvector-post"><a href="https://www.lantern.dev/blog/external-indexing-for-pgvector" target="_blank" rel="noopener">External Indexing for pgvector</a></div>
                <div class="pgvector-post"><a href="https://www.lantern.dev/blog/exploring-postgres-pgvector-hnsw-index-storage" target="_blank" rel="noopener">Exploring Postgres pgvector HNSW Index Storage</a></div>
                <div class="pgvector-post"><a href="https://supabase.com/blog/pgvector-v0-5-0" target="_blank" rel="noopener">pgvector v0.5.0: Faster semantic search with HNSW indexes</a></div>
                <div class="pgvector-post"><a href="https://info.crunchydata.com/blog/early-look-at-hnsw-performance" target="_blank" rel="noopener">Early Look at HNSW Performance with pgvector</a></div>
                <div class="pgvector-post"><a href="https://tembo.io/blog/vector-indexes-in-postgres-using-pgvector-ivfflat-vs-hnsw" target="_blank" rel="noopener">Vector Indexes in Postgres using pgvector: IVFFlat vs HNSW</a></div>
                <div class="pgvector-post"><a href="https://www.tigerdata.com/blog/vector-database-basics-hnsw" target="_blank" rel="noopener">Vector Database Basics: HNSW Index</a></div>
                <div class="pgvector-post"><a href="https://dev.to/azure/postgresql-vector-indexing-hnsw-cosmosdb" target="_blank" rel="noopener">PostgreSQL Vector Indexing with HNSW</a></div>
            </div>
        </div>
    </div>
</div>

<script>
    function openPgvectorModal() {
        const overlay = document.getElementById('pgvectorOverlay');
        overlay.classList.add('active');
        document.body.style.overflow = 'hidden';
    }

    function closePgvectorModal(event) {
        if (!event || event.target === document.getElementById('pgvectorOverlay')) {
            const overlay = document.getElementById('pgvectorOverlay');
            overlay.classList.remove('active');
            document.body.style.overflow = '';
        }
    }

    
    document.addEventListener('keydown', function(event) {
        if (event.key === 'Escape') {
            closePgvectorModal();
        }
    });
</script>
 They all cover the same ground: here&rsquo;s how to install pgvector, here&rsquo;s how to create a vector column, here&rsquo;s a simple similarity search query. Some of them even mention that you should probably add an index.</p>
<p>What they don&rsquo;t tell you is what happens when you actually try to run this in production.</p>
<h2 id="picking-an-index-there-are-no-good-options">Picking an index (there are no good options)</h2>
<p>Let&rsquo;s start with indexes, because this is where the tradeoffs start.</p>
<p>pgvector gives you two index types: IVFFlat and HNSW. The blog posts will tell you that HNSW is newer and generally better, which is&hellip; technically true but deeply unhelpful.</p>
<h3 id="ivfflat">IVFFlat</h3>
<p>IVFFlat (Inverted File with Flat quantization) partitions your vector space into clusters. During search, it identifies the nearest clusters and only searches within those.</p>
<p>The good:</p>
<ul>
<li>Lower memory footprint during index creation</li>
<li>Reasonable query performance for many use cases</li>
<li>Index creation is faster than HNSW</li>
</ul>
<p>The bad:</p>
<ul>
<li>Requires you to specify the number of lists (clusters) upfront</li>
<li>That number significantly impacts both recall and query performance</li>
<li>The commonly recommended formula (<code>rows / 1000</code>) is a starting point at best</li>
<li>Recall can be&hellip; disappointing depending on your data distribution</li>
<li>New vectors get assigned to existing clusters, but clusters don&rsquo;t rebalance without a full rebuild</li>
</ul>
<!-- IMAGE 1: IVFFlat Cluster Visualization
Prompt: Technical diagram showing IVFFlat vector index structure. Show a 2D vector space divided into Voronoi cells/clusters with different colored regions. Include small dots representing vectors clustered within each partition. Label showing 'Query Vector' with arrows pointing to 2-3 nearest clusters that would be searched. Clean, minimal style with a light background. Similar to technical documentation diagrams.
-->
<p><img loading="lazy" src="/posts/the-case-against-pgvector/img_1.png" type="" alt="img_1.png"  />
<em>Image source: <a href="https://unfoldai.com/ivfflat-vs-hnsw/">IVFFlat or HNSW index for similarity search?</a> by Simeon Emanuilov</em></p>
<h3 id="hnsw">HNSW</h3>
<p>HNSW (Hierarchical Navigable Small World) builds a multi-layer graph structure for search.</p>
<p>The good:</p>
<ul>
<li>Better recall than IVFFlat for most datasets</li>
<li>More consistent query performance</li>
<li>Scales well to larger datasets</li>
</ul>
<p>The bad:</p>
<ul>
<li>Significantly higher memory requirements during index builds</li>
<li>Index creation is slow—painfully slow for large datasets</li>
<li>The memory requirements aren&rsquo;t theoretical; they are real, and they&rsquo;ll take down your database if you&rsquo;re not careful</li>
</ul>
<!-- IMAGE 2: HNSW Graph Structure
Prompt: Technical diagram of HNSW hierarchical graph structure showing 3-4 layers. Top layer has sparse nodes with long-range connections, middle layers have medium density, bottom layer is dense with many nodes and local connections. Use different colors for each layer. Show example search path highlighted in a different color traversing from top to bottom. Clean technical style, light background.
-->
<p><img loading="lazy" src="/posts/the-case-against-pgvector/img_2.png" type="" alt="img_2.png"  />
<em>Image source: <a href="https://unfoldai.com/ivfflat-vs-hnsw/">IVFFlat or HNSW index for similarity search?</a> by Simeon Emanuilov</em></p>
<p>None of the blogs mention that building an HNSW index on a few million vectors can consume 10+ GB of RAM or more (depending on your vector dimensions and dataset size). On your production database. While it&rsquo;s running. For potentially hours.</p>
<!-- IMAGE 6: Memory Spike Graph
Prompt: Line graph showing RAM usage over time during HNSW index build on production database. X-axis: Time (0-6 hours), Y-axis: RAM usage (GB). Show baseline at ~8GB labeled 'Normal operations', then sharp spike to 25-30GB labeled 'Index build starts', sustained high usage, then drop back to baseline labeled 'Index complete'. Add horizontal dashed line at server RAM limit (e.g., 32GB) marked 'Danger zone'. Include annotation: 'Production queries still running'. Use typical monitoring dashboard style with blue line and red zones.
-->
<p><img loading="lazy" src="/posts/the-case-against-pgvector/img_6.png" type="" alt="img_6.png"  /></p>
<h2 id="real-time-search-is-basically-impossible">Real-time search is basically impossible</h2>
<p>In a typical application, you want newly uploaded data to be searchable immediately. User uploads a document, you generate embeddings, insert them into your database, and they should be available in search results. Simple, right?</p>
<h3 id="how-index-updates-actually-work">How index updates actually work</h3>
<p>When you insert new vectors into a table with an index, one of two things happens:</p>
<ol>
<li>
<p><strong>IVFFlat</strong>: The new vectors are inserted into the appropriate clusters based on the existing structure. This works, but it means your cluster distribution gets increasingly suboptimal over time. The solution is to rebuild the index periodically. Which means downtime, or maintaining a separate index and doing an atomic swap, or accepting degraded search quality.</p>
</li>
<li>
<p><strong>HNSW</strong>: New vectors are added to the graph structure. This is better than IVFFlat, but it&rsquo;s not free. Each insertion requires updating the graph, which means memory allocation, graph traversals, and potential lock contention.</p>
</li>
</ol>
<p>Neither of these is a deal-breaker in isolation. But here&rsquo;s what happens in practice: you&rsquo;re inserting vectors continuously throughout the day. Each insertion is individually cheap, but the aggregate load adds up. Your database is now handling your normal transactional workload, analytical queries, AND maintaining graph structures in memory for vector search.</p>
<h3 id="handling-new-inserts">Handling new inserts</h3>
<p>Let&rsquo;s say you&rsquo;re building a document search system. Users upload PDFs, you extract text, generate embeddings, and insert them. The user expects to immediately search for that document.</p>
<p>Here&rsquo;s what actually happens:</p>
<p><strong>With no index</strong>: The insert is fast, the document is immediately available, but your searches do a full sequential scan. This works fine for a few thousand documents. At a few hundred thousand? Your searches start taking seconds. Millions? Good luck.</p>
<p><strong>With IVFFlat</strong>: The insert is still relatively fast. The vector gets assigned to a cluster. But whoops, a problem. Those initial cluster assignments were based on the data distribution when you built the index. As you add more data, especially if it&rsquo;s not uniformly distributed, some clusters get overloaded. Your search quality degrades. You rebuild the index periodically to fix this, but during the rebuild (which can take hours for large datasets), what do you do with new inserts? Queue them? Write to a separate unindexed table and merge later?</p>
<p><strong>With HNSW</strong>: The graph gets updated on each insert through incremental insertion, which sounds great. But updating an HNSW graph isn&rsquo;t free—you&rsquo;re traversing the graph to find the right place to insert the new node and updating connections. Each insert acquires locks on the graph structure. Under heavy write load, this becomes a bottleneck. And if your write rate is high enough, you start seeing lock contention that slows down both writes and reads.</p>
<!-- IMAGE 3: Real-time Ingestion Timeline
Prompt: Timeline diagram showing the challenges of real-time vector ingestion. Horizontal timeline with events: 'User uploads document' → 'Generate embeddings' → 'Insert to DB' → 'Index rebuild starts (hours)' with a long bar showing duration → 'New data searchable?'. Show a second parallel timeline of 'More users uploading' with question marks about where those writes go. Use warning colors (amber/orange) for problematic areas. Clean infographic style.
-->
<p><img loading="lazy" src="/posts/the-case-against-pgvector/img_3.jpg" type="" alt="img_3.jpg"  /></p>
<h3 id="the-operational-reality">The operational reality</h3>
<p>Here&rsquo;s the real nightmare: you&rsquo;re not just storing vectors. You have metadata—document titles, timestamps, user IDs, categories, etc. That metadata lives in other tables (or other columns in the same table). You need that metadata and the vectors to stay in sync.</p>
<p>In a normal Postgres table, this is easy—transactions handle it. But when you&rsquo;re dealing with index builds that take hours, keeping everything consistent gets complicated. For IVFFlat, periodic rebuilds are basically required to maintain search quality. For HNSW, you might need to rebuild if you want to tune parameters or if performance has degraded.</p>
<p>The problem is that index builds are memory-intensive operations, and Postgres doesn&rsquo;t have a great way to throttle them. You&rsquo;re essentially asking your production database to allocate multiple (possibly dozens) gigabytes of RAM for an operation that might take hours, while continuing to serve queries.</p>
<p>You end up with strategies like:</p>
<ul>
<li>Write to a staging table, build the index offline, then swap it in (but now you have a window where searches miss new data)</li>
<li>Maintain two indexes and write to both (double the memory, double the update cost)</li>
<li>Build indexes on replicas and promote them</li>
<li>Accept eventual consistency (users upload documents that aren&rsquo;t searchable for N minutes)</li>
<li>Provision significantly more RAM than your &ldquo;working set&rdquo; would suggest</li>
</ul>
<p>None of these are &ldquo;wrong&rdquo; exactly. But they&rsquo;re all workarounds for the fact that pgvector wasn&rsquo;t really designed for high-velocity real-time ingestion.</p>
<h2 id="pre--vs-post-filtering-or-why-you-need-to-become-a-query-planner-expert">Pre- vs. Post-Filtering (or: why you need to become a query planner expert)</h2>
<p>Okay but let&rsquo;s say you solve your index and insert problems.  Now you have a document search system with millions of vectors. Documents have metadata—maybe they&rsquo;re marked as <code>draft</code>, <code>published</code>, or <code>archived</code>. A user searches for something, and you only want to return published documents.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;published&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;-&gt;</span><span class="w"> </span><span class="n">query_vector</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>Simple enough. But now you have a problem: should Postgres filter on status first (pre-filter) or do the vector search first and then filter (post-filter)?</p>
<p>This seems like an implementation detail. It&rsquo;s not. It&rsquo;s the difference between queries that take 50ms and queries that take 5 seconds. It&rsquo;s also the difference between returning the most relevant results and&hellip; not.</p>
<!-- IMAGE 4: Pre-filter vs Post-filter Comparison
Prompt: Side-by-side comparison diagram of pre-filter vs post-filter vector search. Left side labeled 'Pre-filter': shows filter icon → reduced dataset → vector search icon → results. Right side labeled 'Post-filter': shows vector search icon → all results → filter icon → fewer results (with some crossed out). Include example numbers like '1M docs → 100K filtered → top 10' vs '1M docs → top 10 → maybe 2 match filter'. Use flow diagram style with icons and arrows.
-->
<p><img loading="lazy" src="/posts/the-case-against-pgvector/img_4.jpg" type="" alt="img_4.jpg"  /></p>
<p><strong>Pre-filter</strong> works great when the filter is highly selective (1,000 docs out of 10M). It works terribly when the filter isn&rsquo;t selective—you&rsquo;re still searching millions of vectors.</p>
<p><strong>Post-filter</strong> works when your filter is permissive. Here&rsquo;s where it breaks: imagine you ask for 10 results with <code>LIMIT 10</code>. pgvector finds the 10 nearest neighbors, then applies your filter. Only 3 of those 10 are published. You get 3 results back, even though there might be hundreds of relevant published documents slightly further away in the embedding space.</p>
<p>The user searched, got 3 mediocre results, and has no idea they&rsquo;re missing way better matches that didn&rsquo;t make it into the initial k=10 search.</p>
<!-- IMAGE 5: The Recall Problem Visualization
Prompt: Visualization of the filtered search recall problem. Show a 2D scatter plot of documents in embedding space with two colors: green dots for 'published' and gray dots for 'draft/archived'. Draw a circle around the query point showing 'top 10 nearest neighbors' containing mostly gray dots with only 1-2 green dots. Then show several green dots just outside the circle labeled 'Highly relevant published docs (missed)'. Add text: 'User gets 2 results, misses 50+ relevant matches'. Use clean data visualization style.
-->
<p><img loading="lazy" src="/posts/the-case-against-pgvector/img_5.png" type="" alt="img_5.png"  /></p>
<p>You can work around this by fetching more vectors (say, <code>LIMIT 100</code>) and then filtering, but now:</p>
<ul>
<li>You&rsquo;re doing way more distance calculations than needed</li>
<li>You still don&rsquo;t know if 100 is enough</li>
<li>Your query performance suffers</li>
<li>You&rsquo;re guessing at the right oversampling factor</li>
</ul>
<p>With pre-filter, you avoid this problem, but you get the performance problems I mentioned. Pick your poison.</p>
<h3 id="multiple-filters">Multiple filters</h3>
<p>Now add another dimension: you&rsquo;re filtering by user_id AND category AND date_range.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">documents</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">user_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;user123&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">category</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;technical&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">created_at</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="s1">&#39;2024-01-01&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w"></span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="n">embedding</span><span class="w"> </span><span class="o">&lt;-&gt;</span><span class="w"> </span><span class="n">query_vector</span><span class="w">
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="w"></span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span><span class="w">
</span></span></span></code></pre></div><p>What&rsquo;s the right strategy now?</p>
<ul>
<li>Apply all filters first, then search? (Pre-filter)</li>
<li>Search first, then apply all filters? (Post-filter)</li>
<li>Apply some filters first, search, then apply remaining filters? (Hybrid)</li>
<li>Which filters should you apply in which order?</li>
</ul>
<p>The planner will look at table statistics, index selectivity, and estimated row counts and come up with a plan. That plan will probably be wrong, or at least suboptimal, because the planner&rsquo;s cost model wasn&rsquo;t built for vector similarity search.</p>
<p>And it gets worse: you&rsquo;re inserting new vectors throughout the day. Your index statistics are outdated. The plans get increasingly suboptimal until you ANALYZE the table. But ANALYZE on a large table with millions of rows takes time and resources. And it doesn&rsquo;t really understand vector data distribution in a meaningful way—it can tell you how many rows match <code>user_id = 'user123'</code>, but not how clustered those vectors are in the embedding space, which is what actually matters for search performance.</p>
<h3 id="workarounds">Workarounds</h3>
<p>You end up with hacks: query rewriting for different user types, partitioning your data into separate tables, CTE optimization fences to force the planner&rsquo;s hand, or just fetching way more results than needed and filtering in application code.</p>
<p>None of these are sustainable at scale.</p>
<h3 id="what-vector-databases-do">What vector databases do</h3>
<p>Dedicated vector databases have solved this. They understand the cost model of filtered vector search and make intelligent decisions:</p>
<ul>
<li><strong>Adaptive strategies</strong>: Some databases dynamically choose pre-filter or post-filter based on estimated selectivity</li>
<li><strong>Configurable modes</strong>: Others let you specify the strategy explicitly when you know your data distribution</li>
<li><strong>Specialized indexes</strong>: Some build indexes that support efficient filtered search (like filtered HNSW)</li>
<li><strong>Query optimization</strong>: They track statistics specific to vector operations and optimize accordingly</li>
</ul>
<p>OpenSearch&rsquo;s k-NN plugin, for example, lets you specify pre-filter or post-filter behavior. Pinecone automatically handles filter selectivity. Weaviate has optimizations for common filter patterns.</p>
<p>With pgvector, you get to build all of this yourself. Or live with suboptimal queries. Or hire a Postgres expert to spend weeks tuning your query patterns.</p>
<h2 id="hybrid-search-build-it-yourself">Hybrid search? Build it yourself</h2>
<p>Oh, and if you want hybrid search—combining vector similarity with traditional full-text search—you get to build that yourself too.</p>
<p>Postgres has excellent full-text search capabilities. pgvector has excellent vector search capabilities. Combining them in a meaningful way? That&rsquo;s on you.</p>
<p>You need to:</p>
<ul>
<li>Decide how to weight vector similarity vs. text relevance</li>
<li>Normalize scores from two different scoring systems</li>
<li>Tune the balance for your use case</li>
<li>Probably implement Reciprocal Rank Fusion or something similar</li>
</ul>
<p>Again, not impossible. Just another thing that many dedicated vector databases provide out of the box.</p>
<h2 id="pgvectorscale-it-doesnt-solve-everything">pgvectorscale (it doesn&rsquo;t solve everything)</h2>
<p>Timescale has released <a href="https://github.com/timescale/pgvectorscale">pgvectorscale</a>, which addresses some of these issues. It adds:</p>
<ul>
<li>StreamingDiskANN, a new search backend that&rsquo;s more memory-efficient</li>
<li>Better support for incremental index builds</li>
<li>Improved filtering performance</li>
</ul>
<p>This is great! It&rsquo;s also an admission that pgvector out of the box isn&rsquo;t sufficient for production use cases.</p>
<p>pgvectorscale is still relatively new, and adopting it means adding another dependency, another extension, another thing to manage and upgrade. For some teams, that&rsquo;s fine. For others, it&rsquo;s just more evidence that maybe the &ldquo;keep it simple, use Postgres&rdquo; argument isn&rsquo;t as simple as it seemed.</p>
<p>Oh, and if you&rsquo;re running on RDS, pgvectorscale isn&rsquo;t available. AWS doesn&rsquo;t support it. So enjoy managing your own Postgres instance if you want these improvements, or just&hellip; keep dealing with the limitations of vanilla pgvector.</p>
<p>The &ldquo;just use Postgres&rdquo; simplicity keeps getting simpler.</p>
<h2 id="just-use-a-real-vector-database">Just use a real vector database</h2>
<p>I get the appeal of pgvector. Consolidating your stack is good. Reducing operational complexity is good. Not having to manage another database is good.</p>
<p>But here&rsquo;s what I&rsquo;ve learned: for most teams, especially small teams, dedicated vector databases are actually simpler.</p>
<h3 id="what-you-actually-get">What you actually get</h3>
<p>With a managed vector database (Pinecone, Weaviate, Turbopuffer, etc.), you typically get:</p>
<ul>
<li>Intelligent query planning for filtered searches</li>
<li>Hybrid search built in</li>
<li>Real-time indexing without memory spikes</li>
<li>Horizontal scaling without complexity</li>
<li>Monitoring and observability designed for vector workloads</li>
</ul>
<!-- IMAGE 7: pgvector vs Vector DB Comparison
Prompt: Comparison table showing pgvector vs dedicated vector databases. Two columns labeled 'pgvector' and 'Vector DBs (Pinecone, Weaviate, etc.)'. Rows for: 'Filtered search optimization' (❌ vs ✓), 'Hybrid search' (DIY vs Built-in), 'Real-time indexing' (Complex vs Seamless), 'Memory management' (Manual vs Automatic), 'Horizontal scaling' (Limited vs Native), 'Setup complexity' (Lower vs Higher). Use checkmarks, X marks, and simple icons. Clean table design with alternating row colors.
-->
<p><img loading="lazy" src="/posts/the-case-against-pgvector/img_7.jpg" type="" alt="img_7.jpg"  /></p>
<h3 id="its-probably-cheaper-than-you-think">It&rsquo;s probably cheaper than you think</h3>
<p>Yes, it&rsquo;s another service to pay for. But compare:</p>
<ul>
<li>The cost of a managed vector database for your workload</li>
<li>vs. the cost of over-provisioning your Postgres instance to handle index builds</li>
<li>vs. the engineering time to tune queries and manage index rebuilds</li>
<li>vs. the opportunity cost of not building features because you&rsquo;re fighting your database</li>
</ul>
<p>Turbopuffer starts at $64 month with generous limits.</p>
<p>For a lot of teams, the managed service is actually cheaper.</p>
<h2 id="what-i-wish-someone-had-told-me">What I wish someone had told me</h2>
<p>pgvector is an impressive piece of technology. It brings vector search to Postgres in a way that&rsquo;s technically sound and genuinely useful for many applications.</p>
<p>But it&rsquo;s not a panacea. Understand the tradeoffs.</p>
<p>If you&rsquo;re building a production vector search system:</p>
<ol>
<li>
<p><strong>Index management is hard</strong>. Rebuilds are memory-intensive, time-consuming, and disruptive. Plan for this from day one.</p>
</li>
<li>
<p><strong>Query planning matters</strong>. Filtered vector search is a different beast than traditional queries, and Postgres&rsquo;s planner wasn&rsquo;t built for this.</p>
</li>
<li>
<p><strong>Real-time indexing has costs</strong>. Either in memory, in search quality, or in engineering time to manage it.</p>
</li>
<li>
<p><strong>The blog posts are lying to you</strong> (by omission). They&rsquo;re showing you the happy path and ignoring the operational reality.</p>
</li>
<li>
<p><strong>Managed offerings exist for a reason</strong>. There&rsquo;s a reason that Pinecone, Weaviate, Qdrant, and others exist and are thriving. Vector search at scale has unique challenges that general-purpose databases weren&rsquo;t designed to handle.</p>
</li>
</ol>
<p>The question isn&rsquo;t &ldquo;should I use pgvector?&rdquo; It&rsquo;s &ldquo;am I willing to take on the operational complexity of running vector search in Postgres?&rdquo;</p>
<p>For some teams, the answer is yes. You have database expertise, you need the tight integration, you&rsquo;re willing to invest the time.</p>
<p>For many teams—maybe most teams—the answer is probably no. Use a tool designed for the job. Your future self will thank you.</p>
<h2 id="related-reading">Related reading</h2>
<ul>
<li><a href="/posts/rag/">RAG: From Context Injection to Knowledge Integration</a> — why you&rsquo;re storing these embeddings in the first place, and where the pattern breaks down.</li>
<li><a href="/posts/cheesegpt/">CheeseGPT</a> — a small end-to-end RAG build, before any of the production concerns above apply.</li>
<li><a href="/posts/practicalaifeatures/">A Production Framework for LLM Feature Evaluation</a> — deciding which LLM features justify this kind of infrastructure at all.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>A Production Framework for LLM Feature Evaluation</title>
      <link>https://alex-jacobs.com/posts/practicalaifeatures/</link>
      <pubDate>Sun, 01 Jun 2025 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/practicalaifeatures/</guid>
      <description>An empirical analysis of LLM application patterns that successfully scale in production systems, focusing on extraction, generation, and classification use cases</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>After several years of integrating LLMs into production systems, I&rsquo;ve observed a consistent pattern: the features that
deliver real value rarely align with what gets attention at conferences. While the industry focuses on AGI and emergent
behaviors, the mundane applications—data extraction, classification, controlled generation—are quietly transforming how
we build software.</p>
<p>This post presents a framework I&rsquo;ve developed for evaluating LLM features based on what actually ships and scales. It&rsquo;s
deliberately narrow in scope, focusing on patterns that have proven reliable across multiple deployments rather than
exploring the theoretical boundaries of what&rsquo;s possible.</p>
<h2 id="the-three-categories-that-actually-work">The Three Categories That Actually Work</h2>
<p>Through trial, error, and more error, I&rsquo;ve found that LLMs consistently excel in three specific areas. When I&rsquo;m
evaluating a potential AI feature, I ask: &ldquo;Does this clearly fit into one of these categories?&rdquo; If not, it&rsquo;s probably
not worth pursuing (yet).</p>
<h3 id="1-extracting-structured-data-from-unstructured-inputs">1. Extracting Structured Data from Unstructured Inputs</h3>
<p>This is the unsexy workhorse of AI features. Think of it as having an intelligent data entry assistant who never gets
tired of parsing messy inputs.</p>
<p><strong>What makes this valuable:</strong></p>
<ul>
<li>Humans hate data entry</li>
<li>Traditional parsing is brittle and breaks with slight format changes</li>
<li>LLMs can handle ambiguity and variations gracefully</li>
</ul>
<p><strong>Real examples I&rsquo;ve built:</strong></p>
<ul>
<li><strong>PDF to JSON converter</strong>: Taking uploaded forms (PDFs, images, even handwritten docs) and extracting structured data.
What used to require complex OCR pipelines and regex nightmares now works with a simple prompt.</li>
<li><strong>API response mapper</strong>: Taking inconsistent third-party API responses and mapping them to your internal data model.
Every integration engineer&rsquo;s nightmare—different field names, nested structures that change randomly, optional fields
that are sometimes null and sometimes missing entirely.</li>
<li><strong>Customer feedback analyzer</strong>: Extracting actionable insights from the stream of unstructured feedback across emails,
Slack, support tickets. Automatically pulling out feature requests, bug reports, severity, and sentiment. What used to
be a PM&rsquo;s full-time job.</li>
</ul>
<p>The key insight here is that LLMs excel at handling structural variance and ambiguity—the exact things that make
traditional parsers brittle. A single well-crafted prompt can replace hundreds of lines of mapping logic, regex
patterns, and edge case handling. The model&rsquo;s ability to understand intent rather than just pattern match is what makes
this category so powerful.</p>
<p><strong>Production considerations:</strong> For high-volume extraction from standardized formats, purpose-built services
like <a href="https://reducto.ai/">Reducto</a> offer better economics and reliability than raw LLM calls. These platforms have
already solved for edge cases around OCR quality, table extraction, and format variations. The build-vs-buy calculation
here typically favors buying unless you have unique requirements or scale that justifies the engineering investment.</p>
<h3 id="2-content-generation-and-summarization">2. Content Generation and Summarization</h3>
<p>This is probably what most people think of when they hear &ldquo;AI features,&rdquo; but the key is being specific about the use
case.</p>
<p><strong>What makes this valuable:</strong></p>
<ul>
<li>Reduces cognitive load on users</li>
<li>Provides consistent quality and tone</li>
<li>Can process and synthesize large amounts of information quickly</li>
</ul>
<p><strong>Real examples I&rsquo;ve built:</strong></p>
<ul>
<li><strong>Smart report generation</strong>: Taking raw data and generating human-readable reports with insights and recommendations.</li>
<li><strong>Meeting summarizer</strong>: Processing transcripts to extract key decisions, action items, and important discussions.</li>
<li><strong>Documentation assistant</strong>: Generating first drafts of technical documentation from code comments and README files.</li>
</ul>
<p>The critical lesson here is that unconstrained generation is rarely what you want in production. Effective generation
features require explicit boundaries: output structure, length constraints, tone guidelines, and forbidden topics. The
challenge isn&rsquo;t getting the model to generate—it&rsquo;s getting it to generate within your specific constraints reliably.</p>
<p>This is where prompt engineering transitions from art to engineering: defining schemas, enforcing structural
requirements, and building validation layers. The most successful generation features I&rsquo;ve seen treat the LLM as one
component in a larger pipeline, not a magic box.</p>
<h3 id="3-categorization-and-classification">3. Categorization and Classification</h3>
<p>This is where LLMs really shine compared to traditional ML. What used to require thousands of labeled examples and
complex training pipelines can now be done with a well-crafted prompt.</p>
<p><strong>What makes this valuable:</strong></p>
<ul>
<li>No need for labeled training data</li>
<li>Can handle edge cases and ambiguity</li>
<li>Easy to adjust categories without retraining</li>
</ul>
<p>The architectural advantage here is profound: you&rsquo;re essentially defining classifiers declaratively rather than
imperatively. No training data, no model selection, no hyperparameter tuning—just clear descriptions of your categories.
The model&rsquo;s pre-trained understanding of language and context does the heavy lifting.</p>
<p>This fundamentally changes the iteration cycle. Adding a new category or adjusting definitions happens in minutes, not
weeks. The trade-off is less fine-grained control over the decision boundary, but for most business applications, this
is a feature, not a bug.</p>
<p>That said, &ldquo;just use an LLM&rdquo; isn&rsquo;t automatically the right call for classification. I ran 32 experiments pitting small
LLMs against a fine-tuned BERT encoder — <a href="/posts/beatingbert/">the results</a> are a useful counterweight to this section
when latency and cost matter.</p>
<p><strong>Scaling considerations:</strong> Production deployments require:</p>
<ul>
<li><strong>Structured output guarantees</strong>: Libraries like <a href="https://github.com/pydantic/pydantic-ai">Pydantic AI</a>
and <a href="https://github.com/outlines-dev/outlines">Outlines</a> enforce schema compliance at the token generation level,
eliminating post-processing failures.</li>
<li><strong>Prompt optimization</strong>: <a href="https://github.com/stanfordnlp/dspy">DSPy</a> and similar frameworks apply optimization
techniques to prompt engineering, treating it as a learnable parameter rather than a manual craft.</li>
<li><strong>Evals, Observability, and Error Analysis</strong>: This could and will likely eventually be its own post</li>
</ul>
<h2 id="the-anti-patterns-what-doesnt-work">The Anti-Patterns: What Doesn&rsquo;t Work</h2>
<p>Let me save you some pain by sharing what consistently fails:</p>
<h3 id="1-trying-to-replace-domain-expertise">1. Trying to Replace Domain Expertise</h3>
<p>LLMs are great at general knowledge but terrible at specialized domains without extensive context. If you need deep
expertise, you still need experts.</p>
<h3 id="2-real-time-high-frequency-operations">2. Real-time, High-frequency Operations</h3>
<p>Sub-100ms response times and high-frequency calls remain outside the practical envelope for LLM applications. The
latency floor of current models, even with optimizations like speculative decoding, makes them unsuitable for hot-path
operations.</p>
<h3 id="3-anything-requiring-perfect-accuracy">3. Anything Requiring Perfect Accuracy</h3>
<p>LLMs are probabilistic. If you need 100% accuracy (financial calculations, legal compliance, etc.), use traditional
code.</p>
<h2 id="a-practical-evaluation-framework">A Practical Evaluation Framework</h2>
<p>When someone comes to me with an AI feature idea, here&rsquo;s my checklist:</p>
<table>
<thead>
<tr>
<th>Question</th>
<th>Good Sign</th>
<th>Red Flag</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Does it fit one of the three categories?</strong></td>
<td>Clear fit with examples</td>
<td>&ldquo;It&rsquo;s like ChatGPT but&hellip;&rdquo;</td>
</tr>
<tr>
<td><strong>What&rsquo;s the failure mode?</strong></td>
<td>Graceful degradation</td>
<td>Catastrophic failure</td>
</tr>
<tr>
<td><strong>Can a human do it in &lt;5 minutes?</strong></td>
<td>Yes, but it&rsquo;s tedious</td>
<td>No, requires deep expertise</td>
</tr>
<tr>
<td><strong>Is accuracy critical?</strong></td>
<td>Good enough is fine</td>
<td>Must be 100% correct</td>
</tr>
<tr>
<td><strong>What&rsquo;s the response time requirement?</strong></td>
<td>Seconds are fine</td>
<td>Needs to be instant</td>
</tr>
<tr>
<td><strong>Do we have the data?</strong></td>
<td>Yes, and it&rsquo;s accessible</td>
<td>&ldquo;We&rsquo;ll figure it out&rdquo;</td>
</tr>
</tbody>
</table>
<h2 id="implementation-strategy">Implementation Strategy</h2>
<p>For teams evaluating their first LLM feature, I recommend starting with categorization. The reasoning is purely
pragmatic: it has the clearest evaluation metrics, the most forgiving failure modes, and provides immediate value. You
can validate the approach with a small dataset and scale incrementally.</p>
<p>The implementation complexity is also minimal—you&rsquo;re essentially building a discriminator rather than a generator, which
sidesteps many of the challenges around hallucination, output formatting, and content safety. Most importantly, when
classification confidence is low, you can gracefully fall back to human review without breaking the user experience.</p>
<h2 id="the-reality-of-production-ai">The Reality of Production AI</h2>
<p>The gap between AI demos and production systems remains vast. The features that succeed in production share a common
trait: they augment existing workflows rather than attempting to replace them entirely. They handle the tedious,
error-prone tasks that humans perform inconsistently, freeing cognitive capacity for higher-value work.</p>
<p>This isn&rsquo;t a limitation—it&rsquo;s the current sweet spot for LLM applications. The technology excels at tasks that are
simultaneously too complex for traditional automation but too mundane to justify human attention. Understanding this
paradox is key to building AI features that actually ship.</p>
<h2 id="related-reading">Related reading</h2>
<ul>
<li><a href="/posts/beatingbert/">Beating BERT?</a> — 32 experiments on whether small LLMs actually beat a fine-tuned encoder at classification.</li>
<li><a href="/posts/rag/">RAG: From Context Injection to Knowledge Integration</a> — the architectural limits of the most common LLM feature pattern.</li>
<li><a href="/posts/the-case-against-pgvector/">The Case Against pgvector</a> — what the retrieval layer costs you once it&rsquo;s in production.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>A Computer Made This</title>
      <link>https://alex-jacobs.com/posts/notacomputer/</link>
      <pubDate>Thu, 27 Mar 2025 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/notacomputer/</guid>
      <description>OpenAI&amp;#39;s 4o image generation is a step change in AI capabilities. A look at what reasoning in pixel space means for creative work.</description>
      <content:encoded><![CDATA[<p>The past 24 hours have had me navigating an existential crisis while simultaneously being gaslit by friends, family, and colleagues about what&rsquo;s going on. And that&rsquo;s probably fair of them—I have a tendency to overreact to things, to be a bit dramatic.</p>
<p>I am 100% the guy in this panel right now.</p>
<p><img loading="lazy" src="/posts/notacomputer/img1.jpeg" type="" alt="img1.jpeg"  /></p>
<p>But 4o image generation is insane.</p>
<p>I&rsquo;ve been working in the LLM space since before ChatGPT shifted everything. I&rsquo;ve closely followed the progress. I test every new release. I tell my friends that every AI app they send me is slop. I am not easily impressed.</p>
<p>But this feels like another ChatGPT moment. This isn&rsquo;t just better distribution (is hiding your state-of-the-art model in a Discord chat behind /commands really the best way to get people to use it?).</p>
<p>This feels foundational. It&rsquo;s not just a better diffusion model—it&rsquo;s actual reasoning in pixel space.</p>
<p>I&rsquo;ve been on the fence about whether AGI (whatever that even means) is possible. Can we actually bottle intelligence into an electric rock? But it doesn&rsquo;t take much napkin math to pencil this out a few years. (True believers might ask where I&rsquo;ve been, but rest easy brethren—I am yours now.)</p>
<p>It brings to mind this 100% real needlepoint of an Ilya Sutskever quote.
<img loading="lazy" src="/posts/notacomputer/img_2.png" type="" alt="img_2.png"  /></p>
<p>Trying to game out the second- and third-order effects of an image generation model feels strange, even dumb. Infinite Ghibli? What are you worried about? Ghibli gonna take all the jobs?
<figure>
    <img loading="lazy" src="img_20.png" width="300"/> 
</figure>

If I&rsquo;m a graphic designer, it is over for me today. But I&rsquo;m not, I&rsquo;m a software engineer my job is safe!  I have felt like chicken little screaming into the void about the computers coming for a few years now.  Today I feel some combination of awe and dread. (This probably ends with <a href="https://intercoast.edu/articles/the-looming-electrician-shortage-how-it-could-impact-americas-economy-and-the-future-of-ai/">most of us becoming electricians</a> so we can wire up the data centers)
<figure>
    <img loading="lazy" src="graphic-designer-1.png" width="300"/> 
</figure>

I won&rsquo;t even try to get into what the post-reality-filter stage of society we&rsquo;re about to enter looks like, or what this will do to the meme economy.  (My parents already can&rsquo;t tell the difference between AI-generated and real images.  Maybe I can&rsquo;t either.)</p>
<p>I think this tweet (shared with me by a friend this morning) just about sums it up.
<img loading="lazy" src="/posts/notacomputer/img.png" type="" alt="img.png"  /></p>
<p>But to that, I say:
<img loading="lazy" src="/posts/notacomputer/img_1.png" type="" alt="img_1.png"  /></p>
<p>Below is a series I&rsquo;ve been working on to try and demonstrate this phenomenon I&rsquo;m experiencing. My fiancée and I got engaged last October, and we captured an amazing photo (maybe my favorite picture ever). So I&rsquo;ve been trying to recreate it in every style possible.</p>
<p>The consistency of this model is incredible&ndash;and the content filters are tuned to low right now.  I won&rsquo;t be surprised if by the time you&rsquo;re
reading this, most of these styles will be blocked (already seems to be happening :/ )</p>
<p>Our original photo:
<img loading="lazy" src="/posts/notacomputer/IMG_1156.jpg" type="" alt="IMG_1156.jpg"  /></p>
<p>Lego:
<img loading="lazy" src="/posts/notacomputer/img_3.png" type="" alt="img_3.png"  />
Claymation:
<img loading="lazy" src="/posts/notacomputer/img_4.png" type="" alt="img_4.png"  />
Sesame Street:
<img loading="lazy" src="/posts/notacomputer/img_5.png" type="" alt="img_5.png"  />
Scooby-Doo:
<img loading="lazy" src="/posts/notacomputer/img_10.png" type="" alt="img_10.png"  />
Neon Sign:
<img loading="lazy" src="/posts/notacomputer/img_11.png" type="" alt="img_11.png"  />
Tim Burton:
<img loading="lazy" src="/posts/notacomputer/img_12.png" type="" alt="img_12.png"  />
Hey Arnold:
<img loading="lazy" src="/posts/notacomputer/img_13.png" type="" alt="img_13.png"  />
Victorian Botanical Print:
<img loading="lazy" src="/posts/notacomputer/img_14.png" type="" alt="img_14.png"  />
Wes Anderson:
<img loading="lazy" src="/posts/notacomputer/img_7.png" type="" alt="img_7.png"  />
Pixar:
<img loading="lazy" src="/posts/notacomputer/img_8.png" type="" alt="img_8.png"  />
Vintage Comic:
<img loading="lazy" src="/posts/notacomputer/img_9.png" type="" alt="img_9.png"  />
Peanuts:
<img loading="lazy" src="/posts/notacomputer/img_6.png" type="" alt="img_6.png"  />
&ldquo;Yellow Submarine Family&rdquo;:
<img loading="lazy" src="/posts/notacomputer/img_16.png" type="" alt="img_16.png"  />
Construction Paper:
<img loading="lazy" src="/posts/notacomputer/img_17.png" type="" alt="img_17.png"  />
Architectural Blueprint:
<img loading="lazy" src="/posts/notacomputer/img_18.png" type="" alt="img_18.png"  />
Medieval Manuscript:
<img loading="lazy" src="/posts/notacomputer/medival_manuscript.png" type="" alt="medival_manuscript.png"  />
Street Art Stencil:
<img loading="lazy" src="/posts/notacomputer/street-art-stencil.png" type="" alt="street-art-stencil.png"  />
Pixelated Video Game:
<img loading="lazy" src="/posts/notacomputer/pixelated-video-game.png" type="" alt="pixelated-video-game.png"  />
1960s Style Cartoon:
<img loading="lazy" src="/posts/notacomputer/1960s-style-cartoon.png" type="" alt="1960s-style-cartoon.png"  />
Stop Motion:
<img loading="lazy" src="/posts/notacomputer/stop-motion.png" type="" alt="stop-motion.png"  /></p>
<p>And finally, Ghibli:
<img loading="lazy" src="/posts/notacomputer/img_19.png" type="" alt="img_19.png"  /></p>
<p>Other models could already do this!</p>
<p>No, they couldn&rsquo;t.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>RAG: From Context Injection to Knowledge Integration</title>
      <link>https://alex-jacobs.com/posts/rag/</link>
      <pubDate>Mon, 17 Feb 2025 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/rag/</guid>
      <description>A technical dive into the limitations of current RAG approaches, examining architectural challenges and exploring pathways to more integrated knowledge-aware LLM architectures.</description>
      <content:encoded><![CDATA[<h1 id="retrieval-augmented-generation-architectural-limitations-and-future-directions">Retrieval-Augmented Generation: Architectural Limitations and Future Directions</h1>
<p>Retrieval-Augmented Generation (RAG) has rapidly become a cornerstone in the practical application of Large Language Models (LLMs). Its promise is compelling: to expand LLMs beyond their training data by connecting them to external knowledge sources – from enterprise databases and real-time data streams to proprietary knowledge bases. The allure of RAG lies in its apparent simplicity – augment the LLM&rsquo;s input context with retrieved information, and witness enhanced output quality. However, beneath this layer of simplicity lies a more complex reality&ndash;its a bit of a hack. RAG only works because LLMs are generally robust. The more you think on it, the more it becomes clear it <em>shouldn&rsquo;t</em> really work, and should serve only as a stepping stone to a new paradigm.</p>
<h2 id="generation-vs-retrieval">Generation vs. Retrieval</h2>
<p>At their core, LLMs are generative models that produce text by navigating through a high-dimensional latent space. During pre-training on large datasets, these models learn to map language into this space, capturing relationships between words, phrases, and concepts. Text generation isn&rsquo;t a simple lookup process - it&rsquo;s a sequential operation where the model predicts each token based on both the previous context and its learned representations.</p>
<p>RAG changes this core process significantly. Rather than relying only on the model&rsquo;s learned representations, RAG injects external information directly into the context window alongside the user&rsquo;s query. While this works well in practice, it raises important questions about the theoretical and architectural implications:</p>
<ol>
<li>
<p><strong>Impact on Generation Quality:</strong> How does inserting external information affect the model&rsquo;s learned generation process? Does mixing training-derived and retrieved information create inconsistencies in the model&rsquo;s outputs?</p>
</li>
<li>
<p><strong>Information Integration:</strong> Can the model effectively combine information from different sources during generation? Or is it simply stitching together pieces without truly understanding how they relate?</p>
</li>
<li>
<p><strong>Architectural Fitness:</strong> Are transformer architectures and their training objectives actually suited for combining retrieved information with generation? Or are we forcing an approach that doesn&rsquo;t align with how these models were designed to work?</p>
</li>
</ol>
<h2 id="real-world-limitations">Real-World Limitations</h2>
<p>These theoretical concerns manifest in several practical ways:</p>
<h3 id="1-context-integration-problems">1. Context Integration Problems</h3>
<p>Current RAG implementations often struggle with:</p>
<ul>
<li>Abrupt transitions between retrieved content and generated text</li>
<li>Inconsistent voice and style when mixing sources</li>
<li>Difficulty maintaining coherent reasoning across retrieved facts</li>
<li>Limited ability to synthesize information from multiple sources</li>
</ul>
<h3 id="2-attention-mechanism-overload">2. Attention Mechanism Overload</h3>
<p>The transformer&rsquo;s attention mechanism faces significant challenges:</p>
<ul>
<li>Managing attention across disconnected chunks of information</li>
<li>Balancing focus between query, retrieved content, and generated text</li>
<li>Handling potentially contradictory information from different sources</li>
<li>Maintaining coherence when dealing with multiple retrieved documents</li>
</ul>
<h3 id="3-knowledge-conflicts">3. Knowledge Conflicts</h3>
<p>RAG systems often struggle to resolve conflicts between:</p>
<ul>
<li>The model&rsquo;s pretrained knowledge</li>
<li>Retrieved information</li>
<li>Different retrieved sources</li>
<li>User queries and retrieved content</li>
</ul>
<h2 id="the-path-forward-beyond-basic-rag">The Path Forward: Beyond Basic RAG</h2>
<p>Recent research and development suggest several promising directions for addressing these limitations:</p>
<h3 id="1-improved-knowledge-integration">1. Improved Knowledge Integration</h3>
<p>Future systems might:</p>
<ul>
<li>Process retrieved information before injection</li>
<li>Maintain explicit source tracking throughout generation</li>
<li>Use structured knowledge representations</li>
<li>Implement hierarchical attention mechanisms</li>
</ul>
<h3 id="2-enhanced-source-handling">2. Enhanced Source Handling</h3>
<p>Advanced approaches could:</p>
<ul>
<li>Evaluate source reliability and relevance</li>
<li>Resolve conflicts between sources</li>
<li>Maintain provenance information</li>
<li>Generate explicit citations and references</li>
</ul>
<h3 id="3-architectural-innovations">3. Architectural Innovations</h3>
<p>New architectures might include:</p>
<ul>
<li>Dedicated pathways for retrieved information</li>
<li>Specialized attention mechanisms for source integration</li>
<li>Dynamic context window management</li>
<li>Explicit fact-checking mechanisms</li>
</ul>
<h2 id="the-next-evolution-anthropics-citations-api">The Next Evolution: Anthropic&rsquo;s Citations API</h2>
<p>Anthropic&rsquo;s Citations API represents a significant step beyond traditional RAG implementations. While the exact implementation details aren&rsquo;t public, we can make informed speculations about its architectural innovations based on the capabilities it demonstrates.</p>
<h3 id="architectural-innovations">Architectural Innovations</h3>
<p>The Citations API likely goes beyond simple prompt engineering to include fundamental architectural changes:</p>
<ol>
<li>
<p><strong>Enhanced Context Processing</strong></p>
<ul>
<li>Specialized attention mechanisms for source document processing</li>
<li>Dedicated layers for maintaining source awareness throughout generation</li>
<li>Architectural separation between query processing and source document handling</li>
<li>Advanced chunking and document representation strategies</li>
</ul>
</li>
<li>
<p><strong>Citation-Aware Generation</strong></p>
<ul>
<li>Built-in tracking of source-claim relationships</li>
<li>Automatic detection of when citations are needed</li>
<li>Dynamic weighting of source relevance</li>
<li>Real-time fact verification against sources</li>
</ul>
</li>
<li>
<p><strong>Training Innovations</strong></p>
<ul>
<li>Custom loss functions for citation accuracy</li>
<li>Source fidelity metrics during training</li>
<li>Explicit training for source grounding</li>
<li>Specialized datasets for citation learning</li>
</ul>
</li>
</ol>
<h3 id="speculation-on-implementation">Speculation on Implementation</h3>
<p>The system likely employs several key mechanisms:</p>
<ol>
<li>
<p><strong>Dual-Stream Processing</strong></p>
<ul>
<li>Separate processing paths for user queries and source documents</li>
<li>Specialized attention heads for citation tracking</li>
<li>Fusion layers for combining information streams</li>
<li>Dynamic context management</li>
</ul>
</li>
<li>
<p><strong>Source Integration</strong></p>
<ul>
<li>Fine-grained document chunking</li>
<li>Semantic similarity tracking</li>
<li>Citation boundary detection</li>
<li>Provenance preservation</li>
</ul>
</li>
<li>
<p><strong>Training Approach</strong></p>
<ul>
<li>Multi-task training combining generation and citation</li>
<li>Custom datasets focused on source grounding</li>
<li>Citation-specific loss functions</li>
<li>Source fidelity metrics</li>
</ul>
</li>
</ol>
<h2 id="beyond-traditional-rag">Beyond Traditional RAG</h2>
<p>The Citations API and similar emerging technologies point to a future where knowledge integration isn&rsquo;t just an add-on but a core capability of language models. This evolution requires moving beyond simply stuffing context windows with retrieved documents toward architectures specifically designed for knowledge-aware generation.</p>
<p>The next generation of these systems will likely feature:</p>
<ul>
<li>Native citation capabilities</li>
<li>Real-time fact verification</li>
<li>Seamless source integration</li>
<li>Dynamic knowledge updates</li>
<li>Explicit handling of source conflicts</li>
</ul>
<p>As we move forward, the goal isn&rsquo;t to patch the limitations of current RAG systems but to fundamentally rethink how we combine language models with external knowledge. This might lead to entirely new architectures specifically designed for knowledge-enhanced generation, moving us beyond the current paradigm of context window injection toward truly integrated knowledge-aware AI systems.</p>
<h2 id="related-reading">Related reading</h2>
<ul>
<li><a href="/posts/cheesegpt/">CheeseGPT</a> — a simple, end-to-end RAG system built from scratch, if you want the concrete version of everything above.</li>
<li><a href="/posts/the-case-against-pgvector/">The Case Against pgvector</a> — the retrieval half of RAG has to run somewhere, and the storage layer has sharper edges in production than most tutorials admit.</li>
<li><a href="/posts/practicalaifeatures/">A Production Framework for LLM Feature Evaluation</a> — which LLM features actually survive contact with production.</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Python Async Programming: A Deep Dive into async/await and the Event Loop</title>
      <link>https://alex-jacobs.com/posts/pythonasync/</link>
      <pubDate>Sun, 28 Jan 2024 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/pythonasync/</guid>
      <description>How async Python actually works: what async def and await do, how the asyncio event loop schedules coroutines, tasks and futures explained, and when to use async instead of threads or processes.</description>
      <content:encoded><![CDATA[<h2 id="the-short-version">The short version</h2>
<p>If you just want the mental model:</p>
<ul>
<li><strong><code>async def</code> defines a coroutine function.</strong> Calling it doesn&rsquo;t run anything — it returns a coroutine object, which is a description of work, not the work itself.</li>
<li><strong><code>await</code> pauses the current coroutine</strong> and hands control back to the event loop, which is then free to run something else until the awaited thing finishes.</li>
<li><strong>The event loop is a single-threaded scheduler.</strong> There is no parallelism in async Python. It&rsquo;s very efficient turn-taking, and the turns only change at an <code>await</code>.</li>
<li><strong>Async helps when you&rsquo;re I/O-bound</strong> — waiting on networks, APIs, databases, disks. It does nothing for CPU-bound work; that&rsquo;s what <code>multiprocessing</code> is for.</li>
<li><strong>One blocking call stalls everything.</strong> A <code>time.sleep()</code> or a synchronous DB driver anywhere in a coroutine freezes the entire loop, not just that coroutine.</li>
</ul>
<p>The rest of this post is about why each of those is true, and what&rsquo;s happening underneath.</p>
<h2 id="why-async-python-exists">Why async Python exists</h2>
<p>Modern applications — especially anything dealing with network requests or many concurrent operations — need to handle multiple things at once. For a long time, threading and multiprocessing were the go-to answers in Python. But the Global Interpreter Lock and the overhead of thread management limit how well those approaches work, particularly for I/O-bound workloads where the CPU is mostly sitting idle waiting on something external.</p>
<p>Async is the alternative. The surface syntax of <code>async</code> and <code>await</code> looks simple enough, but understanding what&rsquo;s happening beneath it is what separates working async code from code that mysteriously runs no faster than the synchronous version. We&rsquo;ll cover the event loop, coroutines, tasks, futures, context switching, and how async compares to threads and processes.</p>
<h2 id="async-def-and-await-the-syntax"><code>async def</code> and <code>await</code>: the syntax</h2>
<p>These two keywords are the building blocks for writing asynchronous code in Python.</p>
<h3 id="what-async-def-actually-does">What <code>async def</code> actually does</h3>
<p>In Python, you declare a function as asynchronous by using the <code>async def</code> syntax instead of the regular <code>def</code>.  This seemingly small change has profound implications.  An <code>async def</code> function, also known as a <strong>coroutine function</strong>, doesn&rsquo;t execute like a regular synchronous function. Instead, it returns a <strong>coroutine object</strong>.</p>
<p>Think of a coroutine object as a promise of work to be done later. It&rsquo;s not the work itself, but rather a representation of that work, ready to be executed when the time is right.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">hello_async</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Hello from async function!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># Simulate some async operation (like network I/O)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Async function finished.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># Calling the async function returns a coroutine object</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">coro</span> <span class="o">=</span> <span class="n">hello_async</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Coroutine object: </span><span class="si">{</span><span class="n">coro</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># To actually run the coroutine, we need to use an event loop</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">coro</span><span class="p">)</span>
</span></span></code></pre></div><p>In this example, <code>hello_async</code> is an asynchronous function.  When we call <code>hello_async()</code>, it doesn&rsquo;t immediately print &ldquo;Hello from async function!&rdquo;. Instead, it creates and returns a coroutine object. To actually execute the code within the coroutine, we need to use <code>asyncio.run()</code>, which sets up and runs an <strong>event loop</strong> (more on event loops shortly) to manage the execution of our coroutine.</p>
<h3 id="what-await-actually-does">What <code>await</code> actually does</h3>
<p>The <code>await</code> keyword is the other half of the async duo.  It can only be used inside <code>async def</code> functions.  <code>await</code> is the point where an asynchronous function can <strong>pause</strong> its execution and yield control back to the event loop.  This is the crucial mechanism that enables concurrency in async Python without relying on threads.</p>
<p>When you <code>await</code> something, you are essentially saying: &ldquo;I need to wait for this asynchronous operation to complete.  While I&rsquo;m waiting, I&rsquo;m going to yield control back to the event loop so it can work on other tasks. Once this operation is done, please resume my execution from right here.&rdquo;</p>
<p>In our <code>hello_async</code> example, <code>await asyncio.sleep(1)</code> is a simulated asynchronous operation that represents waiting for 1 second.  During this second, the <code>hello_async</code> coroutine pauses, and the event loop is free to execute other coroutines or handle other events.  Once the sleep duration is over, the event loop resumes the <code>hello_async</code> coroutine from the line after the <code>await</code> statement, and it prints &ldquo;Async function finished.&rdquo;</p>
<p><strong>Important Note:</strong>  You can only <code>await</code> objects that are <strong>awaitable</strong>.  In practice, this usually means you&rsquo;re awaiting other coroutines, <code>Future</code> objects (which we&rsquo;ll discuss later), or objects that have implemented the <code>__await__</code> special method.  Standard synchronous functions are <em>not</em> awaitable.</p>
<h3 id="a-complete-async-python-example">A complete async Python example</h3>
<p>Here&rsquo;s the thing the syntax alone doesn&rsquo;t show you: <code>await</code> on its own doesn&rsquo;t buy you any concurrency. If you <code>await</code> three things one after another, you wait for all three in sequence — exactly like synchronous code, just with extra keywords.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">name</span><span class="p">,</span> <span class="n">seconds</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2">: starting&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="n">seconds</span><span class="p">)</span>   <span class="c1"># stands in for a network call</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2">: done&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">name</span><span class="si">}</span><span class="s2"> result&#34;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">sequential</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">start</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">await</span> <span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;A&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">await</span> <span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;B&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">await</span> <span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;C&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;sequential took </span><span class="si">{</span><span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span> <span class="o">-</span> <span class="n">start</span><span class="si">:</span><span class="s2">.1f</span><span class="si">}</span><span class="s2">s&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">concurrent</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="n">start</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;A&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;B&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;C&#34;</span><span class="p">,</span> <span class="mi">1</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;concurrent took </span><span class="si">{</span><span class="n">time</span><span class="o">.</span><span class="n">perf_counter</span><span class="p">()</span> <span class="o">-</span> <span class="n">start</span><span class="si">:</span><span class="s2">.1f</span><span class="si">}</span><span class="s2">s&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">return</span> <span class="n">results</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">
</span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">sequential</span><span class="p">())</span>   <span class="c1"># sequential took 3.0s</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">concurrent</span><span class="p">())</span>   <span class="c1"># concurrent took 1.0s</span>
</span></span></code></pre></div><p>Same coroutines, same <code>await</code>, 3x difference. The concurrency comes from <code>asyncio.gather()</code> handing the event loop three coroutines at once, so it can start all three and then sit on the <code>await</code> while every one of them is in flight. In the sequential version the loop only ever knows about one at a time.</p>
<p>This is the single most common mistake in async Python: writing <code>await</code> in a loop and wondering why nothing got faster.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Slow — one at a time, despite being &#34;async&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">for</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">urls</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">results</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="k">await</span> <span class="n">fetch_url</span><span class="p">(</span><span class="n">url</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Fast — all in flight at once</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">gather</span><span class="p">(</span><span class="o">*</span><span class="p">(</span><span class="n">fetch_url</span><span class="p">(</span><span class="n">url</span><span class="p">)</span> <span class="k">for</span> <span class="n">url</span> <span class="ow">in</span> <span class="n">urls</span><span class="p">))</span>
</span></span></code></pre></div><p>One more trap worth knowing early. Because the event loop is single-threaded, a <em>blocking</em> call inside a coroutine doesn&rsquo;t just block that coroutine, it blocks everything:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">bad</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">time</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>          <span class="c1"># blocks the entire event loop for a full second</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">good</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span> <span class="c1"># yields control; other coroutines run</span>
</span></span></code></pre></div><p>The same applies to any synchronous library — a non-async database driver, <code>requests</code> instead of <code>aiohttp</code>, a heavy CPU computation. If you must call blocking code, push it off the loop with <code>asyncio.to_thread()</code>.</p>
<h2 id="the-asyncio-event-loop">The asyncio event loop</h2>
<p>At the heart of async Python lies the <strong>event loop</strong>.  Think of the event loop as the central conductor of an orchestra.  It&rsquo;s responsible for managing and scheduling the execution of all your asynchronous tasks.  It&rsquo;s a single-threaded loop that constantly monitors for events and dispatches tasks to be executed when those events occur.</p>
<h3 id="how-the-event-loop-works">How the event loop works</h3>
<ol>
<li><strong>Task Queue:</strong> The event loop maintains a queue of tasks (usually coroutines wrapped in <code>Task</code> objects) that are ready to be executed or resumed.</li>
<li><strong>Event Monitoring:</strong> The event loop also monitors for various events, such as network sockets becoming ready for reading or writing, timers expiring, or file operations completing.  It typically uses efficient system calls like <code>select</code>, <code>poll</code>, or <code>epoll</code> (depending on the operating system) to monitor these events without blocking.</li>
<li><strong>Task Execution and Resumption:</strong> When an event occurs that makes a task ready to proceed (e.g., data is available on a socket that a task is waiting to read from), the event loop picks up that task from the queue and executes it until it encounters an <code>await</code> statement.</li>
<li><strong>Yielding Control with <code>await</code>:</strong> When a coroutine reaches an <code>await</code> statement, it effectively tells the event loop, &ldquo;I need to wait for this operation.  Please pause me and let someone else run.&rdquo;  The event loop then takes control and looks for other tasks in the queue that are ready to run.</li>
<li><strong>Resuming Execution:</strong> Once the awaited operation completes (e.g., the network request returns, the timer expires), the event loop is notified.  It then puts the paused coroutine back into the task queue, ready to be resumed at the point where it left off.</li>
<li><strong>Looping Continuously:</strong> The event loop continues this process of monitoring events, executing tasks, and pausing/resuming coroutines in a loop until there are no more tasks to run or the program is explicitly stopped.</li>
</ol>
<h3 id="asyncio-uvloop-and-other-event-loop-implementations">asyncio, uvloop, and other event loop implementations</h3>
<p>Python&rsquo;s standard library provides the <code>asyncio</code> module, which includes a built-in event loop implementation. This default event loop is written in Python and is generally sufficient for many use cases.</p>
<p>However, for performance-critical applications, especially those dealing with high-performance networking, you might consider using alternative event loop implementations.  One popular option is <strong><code>uvloop</code></strong>.</p>
<p><strong><code>uvloop</code></strong>: <code>uvloop</code> is a blazing-fast, drop-in replacement for <code>asyncio</code>&rsquo;s event loop. It&rsquo;s written in Cython and built on top of <code>libuv</code>, the same high-performance library that powers Node.js.  <code>uvloop</code> is significantly faster than the default <code>asyncio</code> event loop, especially for network I/O.</p>
<p>To use <code>uvloop</code>, you typically need to install it separately (<code>pip install uvloop</code>) and then set it as the event loop policy for <code>asyncio</code> when your application starts:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">uvloop</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Running with uvloop!&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Done.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">uvloop</span><span class="o">.</span><span class="n">install</span><span class="p">()</span> <span class="c1"># Set uvloop as the event loop policy</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</span></span></code></pre></div><p>Other event loop implementations exist, but <code>asyncio</code> and <code>uvloop</code> are the most commonly used in Python async programming.  Choosing between them often depends on the performance requirements of your application. For most general async tasks, <code>asyncio</code>&rsquo;s default loop is perfectly adequate.  For high-load network applications, <code>uvloop</code> can provide a noticeable performance boost.</p>
<h2 id="coroutines-tasks-and-futures">Coroutines, tasks, and futures</h2>
<p>To truly understand async Python, it&rsquo;s helpful to think about it in layers.  We&rsquo;ve already touched upon coroutines and the event loop. Let&rsquo;s now delve into the roles of <strong>Tasks</strong> and <strong>Futures</strong>.</p>
<h3 id="coroutines">Coroutines</h3>
<p>As we discussed earlier, coroutines are the asynchronous functions you define using <code>async def</code>. They represent units of asynchronous work.  Coroutines themselves are not directly executed by the event loop. Instead, they need to be wrapped in something that the event loop can manage and schedule.  This &ldquo;something&rdquo; is a <strong>Task</strong>.</p>
<h3 id="tasks-running-coroutines-concurrently">Tasks: running coroutines concurrently</h3>
<p>A <strong>Task</strong> in <code>asyncio</code> is essentially a wrapper around a coroutine that allows the event loop to schedule and manage its execution.  When you want to run a coroutine concurrently within the event loop, you typically create a Task from it.</p>
<p>You can create a Task using <code>asyncio.create_task()</code>:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">my_coroutine</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Coroutine started&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">sleep</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Coroutine finished&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">return</span> <span class="s2">&#34;Coroutine result&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">task1</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">create_task</span><span class="p">(</span><span class="n">my_coroutine</span><span class="p">())</span> <span class="c1"># Create a Task from the coroutine</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">task2</span> <span class="o">=</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">create_task</span><span class="p">(</span><span class="n">my_coroutine</span><span class="p">())</span> <span class="c1"># Create another Task</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Tasks created, waiting for completion...&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">result1</span> <span class="o">=</span> <span class="k">await</span> <span class="n">task1</span> <span class="c1"># Await the completion of task1</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">result2</span> <span class="o">=</span> <span class="k">await</span> <span class="n">task2</span> <span class="c1"># Await the completion of task2</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Task 1 result: </span><span class="si">{</span><span class="n">result1</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Task 2 result: </span><span class="si">{</span><span class="n">result2</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</span></span></code></pre></div><p>In this example, we create two Tasks, <code>task1</code> and <code>task2</code>, from the same <code>my_coroutine</code>.  <code>asyncio.create_task()</code> schedules these coroutines to be run by the event loop concurrently. When we <code>await task1</code> and <code>await task2</code>, we are waiting for these Tasks to complete and retrieve their results.</p>
<p>Tasks are essential for managing the lifecycle of coroutines within the event loop. They provide methods to:</p>
<ul>
<li><strong>Cancel a Task:</strong>  <code>task.cancel()</code></li>
<li><strong>Check if a Task is done:</strong> <code>task.done()</code></li>
<li><strong>Get the result of a Task:</strong> <code>task.result()</code> (if done)</li>
<li><strong>Get exceptions raised during Task execution:</strong> <code>task.exception()</code> (if any)</li>
</ul>
<h3 id="futures-results-that-havent-arrived-yet">Futures: results that haven&rsquo;t arrived yet</h3>
<p>A <strong>Future</strong> is an object that represents the eventual result of an asynchronous operation.  It&rsquo;s a placeholder for a value that might not be available yet.  Tasks in <code>asyncio</code> are actually a subclass of Futures.</p>
<p>Futures are used extensively in async Python to represent the outcome of operations that are performed asynchronously, such as:</p>
<ul>
<li><strong>Network I/O:</strong>  Reading data from a socket, sending a request to a server.</li>
<li><strong>File I/O:</strong> Reading or writing to a file (in an async context).</li>
<li><strong>Concurrent computations:</strong>  Tasks running in parallel (within the same event loop or in different threads/processes).</li>
</ul>
<p>A Future object has a state that can be:</p>
<ul>
<li><strong>Pending:</strong> The asynchronous operation is still in progress.</li>
<li><strong>Running:</strong>  The operation is currently being executed.</li>
<li><strong>Done:</strong> The operation has completed successfully or with an exception.</li>
<li><strong>Cancelled:</strong> The operation has been cancelled.</li>
</ul>
<p>You can interact with a Future to:</p>
<ul>
<li><strong>Check if it&rsquo;s done:</strong> <code>future.done()</code></li>
<li><strong>Get the result:</strong> <code>future.result()</code> (blocks until done if pending, raises exception if an exception occurred)</li>
<li><strong>Get exceptions:</strong> <code>future.exception()</code> (returns exception if one occurred, otherwise <code>None</code>)</li>
<li><strong>Add callbacks:</strong> <code>future.add_done_callback(callback_function)</code> (run a function when the future is done)</li>
<li><strong>Cancel the future:</strong> <code>future.cancel()</code></li>
</ul>
<p>When you <code>await</code> a Task (or any awaitable Future-like object), you are essentially waiting for that Future to become &ldquo;done&rdquo; and then retrieving its result or handling any exceptions.</p>
<h2 id="how-coroutines-pause-and-resume">How coroutines pause and resume</h2>
<p>Now let&rsquo;s look at how coroutines are actually executed, and how context switching works in async Python.</p>
<h3 id="cooperative-vs-preemptive-multitasking">Cooperative vs. preemptive multitasking</h3>
<p>Async Python uses <strong>cooperative multitasking</strong>.  This is in contrast to <strong>preemptive multitasking</strong> used by operating systems for threads and processes.</p>
<ul>
<li><strong>Preemptive Multitasking (Threads/Processes):</strong> In preemptive multitasking, the operating system&rsquo;s scheduler decides when to switch between threads or processes.  It can interrupt a running thread/process at any time and switch to another, even if the running thread/process doesn&rsquo;t explicitly yield control.  This is typically based on time slices and priority levels.</li>
<li><strong>Cooperative Multitasking (Async Python):</strong> In cooperative multitasking, coroutines voluntarily yield control back to the event loop when they encounter an <code>await</code> statement.  The event loop then decides which coroutine to run next.  Context switching only happens at these explicit <code>await</code> points.  A coroutine will continue to run until it reaches an <code>await</code> or completes.</li>
</ul>
<p>This cooperative nature has important implications:</p>
<ul>
<li><strong>No True Parallelism (within a single event loop):</strong>  Within a single event loop running in a single thread, true parallelism is not achieved.  Coroutines take turns running.  If a coroutine doesn&rsquo;t <code>await</code> frequently and performs long-running CPU-bound operations, it can block the event loop and prevent other coroutines from making progress.</li>
<li><strong>Responsiveness:</strong> Cooperative multitasking is excellent for I/O-bound tasks. While one coroutine is waiting for I/O, another can run, keeping the application responsive.</li>
<li><strong>Less Overhead:</strong> Context switching in cooperative multitasking is generally lighter than preemptive context switching between threads or processes. There&rsquo;s less operating system overhead involved.</li>
<li><strong>Deterministic Behavior (mostly):</strong> Because context switching happens only at explicit <code>await</code> points, the execution flow of async code is often more predictable and easier to reason about compared to multithreaded code, which can have race conditions and unpredictable scheduling.</li>
</ul>
<h3 id="what-happens-at-an-await">What happens at an <code>await</code></h3>
<p>When a coroutine reaches an <code>await</code> statement, several things happen:</p>
<ol>
<li><strong><code>await</code> Expression:</strong> The expression after <code>await</code> (e.g., <code>asyncio.sleep(1)</code>, another coroutine, a Future) must be awaitable.</li>
<li><strong>Yielding Control:</strong> The coroutine effectively &ldquo;pauses&rdquo; its execution at the <code>await</code> point. It returns control back to the event loop.</li>
<li><strong>Event Loop Takes Over:</strong> The event loop becomes active again. It looks at its task queue for other tasks that are ready to run.</li>
<li><strong>Registering for Resumption:</strong>  The coroutine, along with information about where it paused (the line after the <code>await</code>), is registered with the event loop as being &ldquo;waiting&rdquo; for the completion of the awaited operation.</li>
<li><strong>Awaited Operation Proceeds:</strong> The awaited operation (e.g., network request, timer) proceeds asynchronously in the background (often managed by non-blocking system calls).</li>
<li><strong>Event Notification:</strong> When the awaited operation is complete, the event loop receives a notification (e.g., socket becomes readable, timer expires).</li>
<li><strong>Resuming the Coroutine:</strong> The event loop puts the paused coroutine back into the task queue, marked as ready to be resumed.</li>
<li><strong>Coroutine Resumes:</strong> When the event loop gets around to executing this coroutine again, it resumes from the exact point where it was paused (right after the <code>await</code> statement).  It now has access to the result of the awaited operation (if any).</li>
</ol>
<p>This pause-and-resume mechanism is what allows asynchronous code to be written in a seemingly sequential style, even though it&rsquo;s actually being executed in an interleaved and non-blocking manner.</p>
<h2 id="async-vs-threads-vs-multiprocessing-in-python">Async vs. threads vs. multiprocessing in Python</h2>
<p>It&rsquo;s worth being clear about when async is the right choice and when threading or multiprocessing is the better fit. They solve different problems, and reaching for the wrong one is how you end up with async code that runs no faster than the version you replaced.</p>
<h3 id="when-to-use-async-io-bound-work">When to use async: I/O-bound work</h3>
<p>Async Python excels in scenarios where your application is <strong>I/O-bound</strong>.  This means that the primary bottleneck is waiting for external operations to complete, such as:</p>
<ul>
<li><strong>Network requests:</strong>  Fetching data from APIs, making HTTP requests, communicating with databases over a network.</li>
<li><strong>File I/O:</strong>  Reading and writing to files (especially over a network file system).</li>
<li><strong>Waiting for user input:</strong>  In GUI applications or interactive systems.</li>
</ul>
<p>In these cases, the CPU is often idle while waiting for I/O operations. Async Python allows you to utilize this idle time by letting other coroutines run while one is waiting for I/O.  It&rsquo;s highly efficient for handling many concurrent I/O operations with minimal overhead.</p>
<p><strong>Example: Web Server</strong></p>
<p>A web server that handles many concurrent requests is a classic example where async Python shines.  While one request is being processed (which often involves waiting for database queries, external API calls, etc.), the server can be handling other requests concurrently.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">asyncio</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">aiohttp</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">aiohttp</span> <span class="kn">import</span> <span class="n">web</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">fetch_data_from_api</span><span class="p">(</span><span class="n">api_url</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">async</span> <span class="k">with</span> <span class="n">aiohttp</span><span class="o">.</span><span class="n">ClientSession</span><span class="p">()</span> <span class="k">as</span> <span class="n">session</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">async</span> <span class="k">with</span> <span class="n">session</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">api_url</span><span class="p">)</span> <span class="k">as</span> <span class="n">response</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="k">return</span> <span class="k">await</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">handler</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">data</span> <span class="o">=</span> <span class="k">await</span> <span class="n">fetch_data_from_api</span><span class="p">(</span><span class="s2">&#34;https://api.example.com/data&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">return</span> <span class="n">web</span><span class="o">.</span><span class="n">json_response</span><span class="p">(</span><span class="n">data</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">main</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">app</span> <span class="o">=</span> <span class="n">web</span><span class="o">.</span><span class="n">Application</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">add_routes</span><span class="p">([</span><span class="n">web</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;/&#39;</span><span class="p">,</span> <span class="n">handler</span><span class="p">)])</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">runner</span> <span class="o">=</span> <span class="n">web</span><span class="o">.</span><span class="n">AppRunner</span><span class="p">(</span><span class="n">app</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">await</span> <span class="n">runner</span><span class="o">.</span><span class="n">setup</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="n">site</span> <span class="o">=</span> <span class="n">web</span><span class="o">.</span><span class="n">TCPSite</span><span class="p">(</span><span class="n">runner</span><span class="p">,</span> <span class="s1">&#39;localhost&#39;</span><span class="p">,</span> <span class="mi">8080</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="k">await</span> <span class="n">site</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="s2">&#34;Server started at http://localhost:8080&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="k">await</span> <span class="n">asyncio</span><span class="o">.</span><span class="n">Event</span><span class="p">()</span><span class="o">.</span><span class="n">wait</span><span class="p">()</span> <span class="c1"># Keep the server running indefinitely</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="n">asyncio</span><span class="o">.</span><span class="n">run</span><span class="p">(</span><span class="n">main</span><span class="p">())</span>
</span></span></code></pre></div><p>This example uses <code>aiohttp</code>, an async HTTP client and server library.  The <code>fetch_data_from_api</code> coroutine performs an asynchronous HTTP request.  The <code>handler</code> coroutine uses <code>await fetch_data_from_api</code> to fetch data without blocking the server.  The server can handle many requests concurrently, making it highly scalable for I/O-bound web applications.</p>
<h3 id="when-to-use-threads">When to use threads</h3>
<p>Threads, especially when used with Python&rsquo;s <code>threading</code> module, are suitable for tasks that are more <strong>CPU-bound</strong> and can benefit from <strong>concurrency</strong> (even if not true parallelism due to the GIL).</p>
<ul>
<li>
<p><strong>CPU-Bound Tasks:</strong> Tasks that spend most of their time performing computations on the CPU, rather than waiting for I/O.  Examples include:</p>
<ul>
<li>Image processing</li>
<li>Numerical computations</li>
<li>Data analysis</li>
<li>Cryptographic operations</li>
</ul>
</li>
<li>
<p><strong>Concurrency (with GIL limitations):</strong> Python&rsquo;s GIL (Global Interpreter Lock) prevents true parallelism for CPU-bound tasks in standard CPython threads. Only one thread can hold the Python interpreter lock at any given time.  However, threads can still provide concurrency by releasing the GIL during I/O operations or certain blocking system calls.  This can improve responsiveness even for CPU-bound tasks if they involve some I/O or blocking.</p>
</li>
</ul>
<p><strong>Example: CPU-Bound Computation (with threading for concurrency)</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">threading</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="nf">cpu_bound_task</span><span class="p">(</span><span class="n">task_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Task </span><span class="si">{</span><span class="n">task_id</span><span class="si">}</span><span class="s2"> started&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">7</span><span class="p">):</span> <span class="c1"># Simulate CPU-intensive computation</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">result</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">end_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">duration</span> <span class="o">=</span> <span class="n">end_time</span> <span class="o">-</span> <span class="n">start_time</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Task </span><span class="si">{</span><span class="n">task_id</span><span class="si">}</span><span class="s2"> finished in </span><span class="si">{</span><span class="n">duration</span><span class="si">:</span><span class="s2">.4f</span><span class="si">}</span><span class="s2"> seconds, result: </span><span class="si">{</span><span class="n">result</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">def</span> <span class="nf">main_threads</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">threads</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">thread</span> <span class="o">=</span> <span class="n">threading</span><span class="o">.</span><span class="n">Thread</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">cpu_bound_task</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">i</span><span class="p">,))</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">threads</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">thread</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">thread</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">for</span> <span class="n">thread</span> <span class="ow">in</span> <span class="n">threads</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">thread</span><span class="o">.</span><span class="n">join</span><span class="p">()</span> <span class="c1"># Wait for all threads to complete</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="n">main_threads</span><span class="p">()</span>
</span></span></code></pre></div><p>In this example, <code>cpu_bound_task</code> simulates a CPU-intensive operation.  We create multiple threads to run this task concurrently.  While the GIL limits true parallelism for CPU-bound Python code, threads can still provide some concurrency and potential performance improvement, especially if the tasks involve some I/O or blocking operations.  For purely CPU-bound tasks, however, the benefits might be limited by the GIL.</p>
<h3 id="when-to-use-processes-true-parallelism">When to use processes: true parallelism</h3>
<p>For truly <strong>CPU-bound and computationally intensive tasks</strong> that need <strong>true parallelism</strong> and to bypass the GIL limitations, <strong>multiprocessing</strong> using Python&rsquo;s <code>multiprocessing</code> module is the way to go.</p>
<ul>
<li><strong>True Parallelism:</strong> Multiprocessing creates separate processes, each with its own Python interpreter and memory space. Processes run in parallel on multiple CPU cores, achieving true parallelism for CPU-bound tasks.</li>
<li><strong>Bypassing the GIL:</strong> Each process has its own GIL, so the GIL limitation of threads is overcome.</li>
<li><strong>Higher Overhead:</strong> Process creation and inter-process communication have more overhead compared to threads or async tasks. Processes consume more system resources (memory, process management overhead).</li>
</ul>
<p><strong>Example: CPU-Bound Computation (with multiprocessing for parallelism)</strong></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">multiprocessing</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">time</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="k">def</span> <span class="nf">cpu_bound_task_process</span><span class="p">(</span><span class="n">task_id</span><span class="p">):</span> <span class="c1"># Same CPU-bound task as before</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Process </span><span class="si">{</span><span class="n">task_id</span><span class="si">}</span><span class="s2"> started&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">start_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">for</span> <span class="n">_</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">10</span><span class="o">**</span><span class="mi">7</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">result</span> <span class="o">+=</span> <span class="mi">1</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">end_time</span> <span class="o">=</span> <span class="n">time</span><span class="o">.</span><span class="n">time</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">duration</span> <span class="o">=</span> <span class="n">end_time</span> <span class="o">-</span> <span class="n">start_time</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Process </span><span class="si">{</span><span class="n">task_id</span><span class="si">}</span><span class="s2"> finished in </span><span class="si">{</span><span class="n">duration</span><span class="si">:</span><span class="s2">.4f</span><span class="si">}</span><span class="s2"> seconds, result: </span><span class="si">{</span><span class="n">result</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">def</span> <span class="nf">main_processes</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="n">processes</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="mi">4</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">process</span> <span class="o">=</span> <span class="n">multiprocessing</span><span class="o">.</span><span class="n">Process</span><span class="p">(</span><span class="n">target</span><span class="o">=</span><span class="n">cpu_bound_task_process</span><span class="p">,</span> <span class="n">args</span><span class="o">=</span><span class="p">(</span><span class="n">i</span><span class="p">,))</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">processes</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">process</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="n">process</span><span class="o">.</span><span class="n">start</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="k">for</span> <span class="n">process</span> <span class="ow">in</span> <span class="n">processes</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">process</span><span class="o">.</span><span class="n">join</span><span class="p">()</span> <span class="c1"># Wait for all processes to complete</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="n">main_processes</span><span class="p">()</span>
</span></span></code></pre></div><p>In this multiprocessing example, we create separate processes to run the same CPU-bound task.  Because each process has its own interpreter and bypasses the GIL, we can achieve true parallelism and significantly speed up CPU-intensive computations on multi-core systems.</p>
<h3 id="choosing-between-async-threads-and-processes">Choosing between async, threads, and processes</h3>
<table>
<thead>
<tr>
<th>Feature</th>
<th>Async Python (asyncio)</th>
<th>Threads (threading)</th>
<th>Processes (multiprocessing)</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Task Type</strong></td>
<td>I/O-bound</td>
<td>CPU-bound (with I/O)</td>
<td>CPU-bound (pure computation)</td>
</tr>
<tr>
<td><strong>Parallelism</strong></td>
<td>No (within loop)</td>
<td>Limited (due to GIL)</td>
<td>Yes (true parallelism)</td>
</tr>
<tr>
<td><strong>Concurrency</strong></td>
<td>High</td>
<td>Moderate</td>
<td>High</td>
</tr>
<tr>
<td><strong>Overhead</strong></td>
<td>Low</td>
<td>Moderate</td>
<td>High</td>
</tr>
<tr>
<td><strong>GIL Impact</strong></td>
<td>Not affected</td>
<td>Limited by GIL</td>
<td>Bypasses GIL</td>
</tr>
<tr>
<td><strong>Context Switching</strong></td>
<td>Cooperative (light)</td>
<td>Preemptive (OS)</td>
<td>Preemptive (OS)</td>
</tr>
<tr>
<td><strong>Memory Footprint</strong></td>
<td>Lower</td>
<td>Moderate</td>
<td>Higher</td>
</tr>
<tr>
<td><strong>Complexity</strong></td>
<td>Moderate</td>
<td>Moderate</td>
<td>Higher (IPC needed)</td>
</tr>
<tr>
<td><strong>Use Cases</strong></td>
<td>Web servers, network apps, UI</td>
<td>Concurrent I/O + CPU</td>
<td>CPU-intensive computations</td>
</tr>
</tbody>
</table>
<p><strong>General Guidelines:</strong></p>
<ul>
<li><strong>I/O-Bound, High Concurrency:</strong> Async Python (asyncio) is often the best choice.</li>
<li><strong>CPU-Bound with some I/O, Responsiveness:</strong> Threads (threading) can be considered, but be mindful of GIL limitations for pure CPU-bound tasks.</li>
<li><strong>CPU-Bound, True Parallelism, Max Performance:</strong> Processes (multiprocessing) are essential, especially for computationally intensive tasks on multi-core machines.</li>
<li><strong>Hybrid Applications:</strong> You can combine async and multiprocessing. For example, use async for handling network I/O and multiprocessing for CPU-bound background tasks.</li>
</ul>
<h2 id="when-to-reach-for-async">When to reach for async</h2>
<p>Async Python is a good way to write concurrent code for I/O-bound applications, and understanding the machinery underneath — the event loop, coroutines, tasks, futures, cooperative multitasking — is what makes the difference between using it well and using it superstitiously.</p>
<p>It isn&rsquo;t a silver bullet. It won&rsquo;t speed up CPU-bound work, it doesn&rsquo;t replace threading or multiprocessing, and a single blocking call will undo all of it. But for the case it&rsquo;s designed for — lots of concurrent waiting on things outside your process — nothing else in Python comes close on overhead.</p>
<p>If you take three things from this post: <code>async def</code> returns a coroutine rather than running one, concurrency comes from <code>gather</code> and tasks rather than from <code>await</code> itself, and the loop is single-threaded so anything that blocks blocks everyone.</p>
<p>If you want to see these concepts in a real application, FastAPI is async Python end to end — I&rsquo;ve written about <a href="/posts/fastapitests/">integration testing FastAPI</a>, where the event loop and <code>async def</code> shape how you have to structure your test fixtures.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Integration Testing FastAPI: Mocking Auth, MongoDB, S3, and External APIs</title>
      <link>https://alex-jacobs.com/posts/fastapitests/</link>
      <pubDate>Sun, 11 Jun 2023 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/fastapitests/</guid>
      <description>A practical guide to FastAPI integration tests with pytest: mocking JWT authentication and the TestClient, patching external API calls, and faking MongoDB and S3 with mongomock and moto.</description>
      <content:encoded><![CDATA[<p>Follow along with all the code <a href="https://github.com/alexjacobs08/fastApi-Integration-tests">here</a></p>
<h2 id="introduction">Introduction</h2>
<p>If you&rsquo;re a backend developer working with FastAPI, you already know the framework excels at simplifying API development
with features like async support and automated Swagger docs. However, when it comes to integration testing&ndash;particularly
mocking external services like MongoDB, AWS S3, and third-party APIs—the waters can get murky. This post is your guide
to navigating these complexities.</p>
<p>This isn&rsquo;t a one-stop-shop for all things testing; the focus is squarely on integration testing within
FastAPI.</p>
<p><strong>Prerequisites:</strong> Familiarity with FastAPI, PyTest, MongoDB, and AWS S3 is assumed. If you&rsquo;re new to any of these
technologies, you may want to get up to speed before proceeding. FastAPI is built on async Python, and a lot of testing
confusion traces back to not knowing what <code>async def</code> and the event loop are really doing — if that&rsquo;s shaky ground,
my <a href="/posts/pythonasync/">deep dive into Python async programming</a> covers it.</p>
<h3 id="what-is-integration-testing">What is Integration Testing?</h3>
<p>Integration testing involves combining individual units of code and testing them as a group. This type of testing aims
to expose faults in the interactions between integrated units. In our context, we could also probably call these
API tests (and you&rsquo;ll see that&rsquo;s how I name my test files). In the context of FastAPI, these units often involve your
API endpoints, external databases like MongoDB, and other services such as AWS S3.</p>
<h3 id="why-its-challenging">Why It&rsquo;s Challenging</h3>
<p>When compared to unit tests, integration tests in FastAPI present unique difficulties. These challenges mostly stem from
the interaction with external dependencies. Mocking these dependencies is not always straightforward, and mistakes can
lead to false positives or negatives, undermining the purpose of the tests.</p>
<h3 id="why-it-matters">Why It Matters</h3>
<p>So, why focus on integration testing in FastAPI? Because it&rsquo;s here that you validate that various services and databases
work in harmony. By ensuring that these integrated units function as expected, you not only increase code reliability
but also save debugging time in the long run.</p>
<p>Unit tests are great for testing individual units of code, but they don&rsquo;t test the interactions between these units.
I also find integration tests (especially this kind, which test our endpoints) are particularly useful before doing a
large refactor&ndash;going from a v0 to v1 of an app, where unit tests might not make a ton of sense because you&rsquo;re rewriting
all of your &lsquo;units&rsquo;.</p>
<h2 id="a-glimpse-into-our-test-app">A Glimpse into Our Test App</h2>
<p>Before we delve into the technicalities of mocking various elements, let&rsquo;s take a quick look at the FastAPI application
we&rsquo;ll be using as a test subject. This app serves as a sandbox for our integration tests, built to showcase different
aspects of FastAPI that commonly require mocking in a test environment.</p>
<p>Our sample application has a straightforward schema designed for demonstration purposes:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="l">/login </span><span class="w"> </span><span class="c"># simple login</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">POST</span><span class="p">:</span><span class="w"> </span><span class="l">Login</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="l">/users/me/ </span><span class="w"> </span><span class="c"># example for mocking auth</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">GET</span><span class="p">:</span><span class="w"> </span><span class="l">Read Users Me</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="l">/users/me/preferences </span><span class="w"> </span><span class="c"># example for mocking mongodB</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">GET</span><span class="p">:</span><span class="w"> </span><span class="l">Get Preferences</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">POST</span><span class="p">:</span><span class="w"> </span><span class="l">Save Preferences</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="l">/users/me/profile_pic </span><span class="w"> </span><span class="c"># example for mocking s3</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">  </span><span class="nt">GET</span><span class="p">:</span><span class="w"> </span><span class="l">Get Profile Picture</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">  </span><span class="nt">POST</span><span class="p">:</span><span class="w"> </span><span class="l">Save Profile Picture</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="l">/weather/{city} </span><span class="w"> </span><span class="c"># example for mocking external API</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">  </span><span class="nt">GET</span><span class="p">:</span><span class="w"> </span><span class="l">Get Weather</span><span class="w">
</span></span></span></code></pre></div><p>Our tests live in <code>tests/</code> where they are organized according to the application components they
evaluate. We employ PyTest for test execution and adhere to its conventions for discovering tests and initial set up.
This includes utilizing the <code>conftest.py</code> configuration file for shared hooks and fixtures.</p>
<h2 id="mocking-authentication-in-fastapi">Mocking Authentication in FastAPI</h2>
<p>As we move through the article, we&rsquo;ll see that FastAPI provides an easy way to do dependency injection,
which is especially useful for our testing scenarios. Using <code>dependency_overrides</code>, we can inject our mocked functions
into the FastAPI app, but remember, this only works for endpoints/functions using the <code>Depends()</code> syntax.
You can read more about dependency injection in FastAPI <a href="https://fastapi.tiangolo.com/tutorial/dependencies/">here</a>.</p>
<p>In our FastAPI application, we have endpoints that require authentication. During testing, we don&rsquo;t want to use real
authentication because it would require us to manage real user credentials and tokens. This would complicate our tests
and potentially expose sensitive information. Therefore, we mock the authentication process.</p>
<h3 id="testing-our-login">Testing Our Login</h3>
<p>Before we get to dependency injection or mocking out our services, let&rsquo;s make sure our auth works. We have simple
authentication defined in the project that checks if a user is in the database, if their password
matches, and returns a token if they are. (I am not going to focus on how to do authentication, but you can check out
the code in <code>auth.py</code> if you&rsquo;re interested.)</p>
<p>For the purposes of this tutorial, our database is just a dictionary with one hard coded user.</p>
<h6 id="ill-be-using-my-name-as-the-username-in-this-tutorial-in-order-to-burn-it-into-your-brain">(I&rsquo;ll be using my name as the username in this tutorial in order to burn it into your brain)</h6>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: app/db.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">users_db</span><span class="p">:</span> <span class="n">Dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="n">UserInDB</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s2">&#34;alexjacobs&#34;</span><span class="p">:</span> <span class="n">UserInDB</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">username</span><span class="o">=</span><span class="s2">&#34;alexjacobs&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">email</span><span class="o">=</span><span class="s2">&#34;alex@example.com&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">hashed_password</span><span class="o">=</span><span class="n">get_password_hash</span><span class="p">(</span><span class="s2">&#34;secret&#34;</span><span class="p">),</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>and our login endpoint&hellip;</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: app/main.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nd">@app</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/login&#34;</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">Token</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">login</span><span class="p">(</span><span class="n">_login</span><span class="p">:</span> <span class="n">Login</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="n">authenticate_user</span><span class="p">(</span><span class="n">users_db</span><span class="p">,</span> <span class="n">_login</span><span class="o">.</span><span class="n">username</span><span class="p">,</span> <span class="n">_login</span><span class="o">.</span><span class="n">password</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">if</span> <span class="ow">not</span> <span class="n">user</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="n">status_code</span><span class="o">=</span><span class="n">status</span><span class="o">.</span><span class="n">HTTP_401_UNAUTHORIZED</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="n">detail</span><span class="o">=</span><span class="s2">&#34;Incorrect username or password&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;WWW-Authenticate&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">access_token</span> <span class="o">=</span> <span class="n">create_access_token</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="n">data</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;sub&#34;</span><span class="p">:</span> <span class="n">user</span><span class="o">.</span><span class="n">username</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="k">return</span> <span class="p">{</span><span class="s2">&#34;jwt&#34;</span><span class="p">:</span> <span class="n">access_token</span><span class="p">,</span> <span class="s2">&#34;token_type&#34;</span><span class="p">:</span> <span class="s2">&#34;bearer&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>and we have some simple tests just to verify that our login endpoint works as expected</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/test_auth_api.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">test_login</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/login&#34;</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;username&#34;</span><span class="p">:</span> <span class="s2">&#34;alexjacobs&#34;</span><span class="p">,</span> <span class="s2">&#34;password&#34;</span><span class="p">:</span> <span class="s2">&#34;secret&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">assert</span> <span class="s2">&#34;jwt&#34;</span> <span class="ow">in</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">def</span> <span class="nf">test_login_fail</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/login&#34;</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;username&#34;</span><span class="p">:</span> <span class="s2">&#34;alexjacobs&#34;</span><span class="p">,</span> <span class="s2">&#34;password&#34;</span><span class="p">:</span> <span class="s2">&#34;not_the_right_password&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">401</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;detail&#39;</span><span class="p">:</span> <span class="s1">&#39;Incorrect username or password&#39;</span><span class="p">}</span>
</span></span></code></pre></div><p>These initial tests don&rsquo;t require mocking the authentication process because they are designed to test the
endpoint&rsquo;s basic functionality. They act as a foundational layer upon which we will build more complex test scenarios
that require mocking.</p>
<h3 id="mocking-authentication-in-our-test-client">Mocking Authentication in Our Test Client</h3>
<p>Now, let&rsquo;s say we have another endpoint that requires authentication. We&rsquo;ll use the <code>Depends</code> function to inject our
authentication function into our endpoint. Our <code>/users/me</code> endpoint returns information about the user logged in.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: app/main.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nd">@app</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me/&#34;</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">User</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">read_users_me</span><span class="p">(</span><span class="n">_user</span><span class="p">:</span> <span class="n">TokenData</span> <span class="o">=</span> <span class="n">Depends</span><span class="p">(</span><span class="n">get_auth</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">user</span> <span class="o">=</span> <span class="n">users_db</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">_user</span><span class="o">.</span><span class="n">username</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">if</span> <span class="n">user</span> <span class="ow">is</span> <span class="kc">None</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="k">raise</span> <span class="n">HTTPException</span><span class="p">(</span><span class="n">status_code</span><span class="o">=</span><span class="mi">404</span><span class="p">,</span> <span class="n">detail</span><span class="o">=</span><span class="s2">&#34;User not found&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="n">user</span>
</span></span></code></pre></div><p>Authentication is handled by the <code>get_auth</code> function, which is injected into our endpoint using the <code>Depends</code>
function.<br>
Since we don&rsquo;t want to use real authentication in our tests, we&rsquo;re going to mock this function. I&rsquo;m going to show
multiple ways of doing this so you can choose the one that works best for you.</p>
<p>The most standard way of mocking this function is to use the <code>dependency_overrides</code> feature of FastAPI, before we initialize our
TestClient.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">client</span><span class="p">(</span><span class="n">mock_s3_bucket</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="c1"># we patch auth within our client fixture</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kn">from</span> <span class="nn">app.main</span> <span class="kn">import</span> <span class="n">app</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_get_auth</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">return</span> <span class="n">TokenData</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="s2">&#34;alexjacobs&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="p">[</span><span class="n">get_auth</span><span class="p">]</span> <span class="o">=</span> <span class="n">mock_get_auth</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">with</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span> <span class="k">as</span> <span class="n">test_client</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">yield</span> <span class="n">test_client</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
</span></span></code></pre></div><p>You&rsquo;ll see we declare a function called <code>mock_get_auth</code> that returns a <code>TokenData</code> object.  Then in line 10,
<code>app.dependency_overrides[get_auth] = mock_get_auth</code>, we &lsquo;inject&rsquo; our mock function into our app in place of the
<code>get_auth</code> function.</p>
<p>This code is essentially replacing the <code>get_auth</code> function in our app with a mock function that returns the TokenData
object hardcoded into the function we&rsquo;re pathing with.</p>
<p>Then, in our test we pass in the client fixture, and we can see that our test passes.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/test_auth_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_me_patched_auth</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># auth works without real jwt because we patched it in the client fixture</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me&#34;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer &#34;</span> <span class="o">+</span> <span class="s1">&#39;fake.jwt&#39;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;username&#39;</span><span class="p">:</span> <span class="s1">&#39;alexjacobs&#39;</span><span class="p">,</span> <span class="s1">&#39;email&#39;</span><span class="p">:</span> <span class="s1">&#39;alex@example.com&#39;</span><span class="p">}</span>
</span></span></code></pre></div><p>(we really don&rsquo;t even need to include auth headers, since we&rsquo;re mocking the auth function, but I&rsquo;m keeping it
here for clarity)</p>
<h3 id="mocking-authentication-directly-in-your-tests">Mocking Authentication Directly in your Tests</h3>
<p>Now, what if we want to mock the auth function in a different way? We can do this by mocking the auth function directly
in our test.</p>
<p>First, we&rsquo;ll create a second client fixture that doesn&rsquo;t patch the auth function.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">client_unpatched_auth</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="c1"># we don&#39;t patch auth for this client</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="kn">from</span> <span class="nn">app.main</span> <span class="kn">import</span> <span class="n">app</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">with</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span> <span class="k">as</span> <span class="n">test_client_2</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">        <span class="k">yield</span> <span class="n">test_client_2</span>
</span></span></code></pre></div><p>Now we&rsquo;re going to write a test to verify that auth fails (since we&rsquo;re not patching the auth function)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/test_auth_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_me_unpatched_auth</span><span class="p">(</span><span class="n">client_unpatched_auth</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># auth fails without real jwt because we&#39;re using the unpatched client fixture</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client_unpatched_auth</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me&#34;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer &#34;</span> <span class="o">+</span> <span class="s1">&#39;fake.jwt&#39;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">401</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s2">&#34;detail&#34;</span><span class="p">:</span> <span class="s2">&#34;Could not validate credentials&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>To make this work, there are two approaches. The first is to mock the auth function directly in the test</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/test_auth_api.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_me_path_auth_in_fn</span><span class="p">(</span><span class="n">client_unpatched_auth</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="c1"># auth works because we patch it in this test (not in the client fixture)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_get_auth</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">return</span> <span class="n">TokenData</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="s2">&#34;alexjacobs&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="p">[</span><span class="n">get_auth</span><span class="p">]</span> <span class="o">=</span> <span class="n">mock_get_auth</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client_unpatched_auth</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me&#34;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer &#34;</span> <span class="o">+</span> <span class="s1">&#39;fake.jwt&#39;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span></code></pre></div><p>All we&rsquo;ve done here is moved the code that patches the auth function into the test itself. This could be useful if you
wanted to mock the auth function in some tests, but not others (but only wanted to have one client fixture).</p>
<p>Next, we&rsquo;ll do the same thing, but we&rsquo;re going to use a new fixture and factory function to create our mock auth function.
So first, we&rsquo;ll create a new fixture,</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">mock_get_auth_factory</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_get_auth</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">return</span> <span class="n">TokenData</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="s2">&#34;alexjacobs&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="n">mock_get_auth</span>
</span></span></code></pre></div><p>and then we&rsquo;ll use this fixture in our test</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/test_auth_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_me_patch_auth_with_fixture</span><span class="p">(</span><span class="n">client_unpatched_auth</span><span class="p">,</span> <span class="n">mock_get_auth_factory</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="c1"># auth works because we patch it with a fixture instead of in the client fixture</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="p">[</span><span class="n">get_auth</span><span class="p">]</span> <span class="o">=</span> <span class="n">mock_get_auth_factory</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client_unpatched_auth</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me&#34;</span><span class="p">,</span> <span class="n">headers</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;Authorization&#34;</span><span class="p">:</span> <span class="s2">&#34;Bearer &#34;</span> <span class="o">+</span> <span class="s1">&#39;fake.jwt&#39;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span></code></pre></div><p>Notice that when we&rsquo;re passing in the <code>mock_get_auth_factory</code> fixture, we&rsquo;re passing in the function itself, not the
return value of the function. This is because the fixture is a factory function that returns a function, so we need to
pass in the function itself.</p>
<p>This may not seem very useful (and for mocking auth, it may not be), but there are plenty of scenarios when you may
want to mock a function in some tests, but not others. This is a good way to do that.</p>
<h2 id="mocking-external-apis">Mocking External APIs</h2>
<p>Our FastAPI application makes external API calls to fetch weather data. During testing, we don&rsquo;t want to make real API
calls because they can be slow and unreliable. Therefore, we mock the external API calls.</p>
<p>We mock the external API calls by patching the <code>fetch_weather</code> function in our FastAPI application. This function is
responsible for making the actual API call and returning the weather data. We replace it with a mock function that
always returns a predefined weather data. But, this function does use dependency injection (the <code>Depends</code> funciton)
in our app, so we have to mock it in a different way.</p>
<p>First, let&rsquo;s look at our endpoint and the function it calls.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: app/main.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nd">@app</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/weather/</span><span class="si">{city}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="n">response_model</span><span class="o">=</span><span class="n">WeatherResponse</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">async</span> <span class="k">def</span> <span class="nf">get_weather</span><span class="p">(</span><span class="n">city</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">_user</span><span class="p">:</span> <span class="n">TokenData</span> <span class="o">=</span> <span class="n">Depends</span><span class="p">(</span><span class="n">get_auth</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">weather</span> <span class="o">=</span> <span class="n">fetch_weather</span><span class="p">(</span><span class="n">city</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="n">WeatherResponse</span><span class="p">(</span><span class="n">city</span><span class="o">=</span><span class="n">city</span><span class="p">,</span> <span class="n">weather</span><span class="o">=</span><span class="n">weather</span><span class="p">)</span>
</span></span></code></pre></div><p>and our <code>fetch_weather()</code> function</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: app/helpers.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">fetch_weather</span><span class="p">(</span><span class="n">city_name</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">url</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">&#34;http://wttr.in/</span><span class="si">{</span><span class="n">city_name</span><span class="si">}</span><span class="s2">?format=%C+%t&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">requests</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">url</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="n">response</span><span class="o">.</span><span class="n">text</span>
</span></span></code></pre></div><p>Pretty simple&hellip; we are hitting an external API, though, so we need to mock this out in our tests.</p>
<p>We have three ways to mock the external API calls:</p>
<h3 id="mocking-external-apis-with-unittestmockpatch-directly-in-our-test">Mocking External APIs with unittest.mock.patch Directly in Our Test</h3>
<p>Using unittest.mock.patch, we patch our <code>fetch_weather</code> function to return &lsquo;sunny&rsquo;.
We then verify our endpoint returns the expected response (and verify that our mock function was called&ndash;this last part
probably isn&rsquo;t really necessary here, but is a good demonstration)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/test_weather_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_weather</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">with</span> <span class="n">patch</span><span class="p">(</span><span class="s1">&#39;app.main.fetch_weather&#39;</span><span class="p">,</span> <span class="n">return_value</span><span class="o">=</span><span class="s1">&#39;sunny&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">mock_fetch_weather</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/weather/atlanta&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;city&#39;</span><span class="p">:</span> <span class="s1">&#39;atlanta&#39;</span><span class="p">,</span> <span class="s1">&#39;weather&#39;</span><span class="p">:</span> <span class="s1">&#39;sunny&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="n">mock_fetch_weather</span><span class="o">.</span><span class="n">assert_called_once_with</span><span class="p">(</span><span class="s1">&#39;atlanta&#39;</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="creating-a-fixture-that-patches-the-function">Creating a fixture that patches the function</h3>
<p>Let&rsquo;s suggest that we may want to test multiple endpoints that call the <code>fetch_weather</code> function. We could patch the
function in each test, but that&rsquo;s a lot of code duplication. Instead, we can create a fixture that patches the
function. First, we&rsquo;ll set up our fixture (this is another factor function). We&rsquo;re using <code>unittest</code>&rsquo;s Mock and Patch
functions in order to replace the <code>fetch_weather</code> function with a mock function that returns &lsquo;rainy&rsquo;.</p>
<p>Note: As before, we&rsquo;re using a factory function to create our mock function, so we need to pass in the function itself,
not the return value of the function.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">mock_fetch_weather_factory</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_fetch_weather</span><span class="p">(</span><span class="n">city</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">return</span> <span class="s2">&#34;rainy&#34;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">mock</span> <span class="o">=</span> <span class="n">Mock</span><span class="p">(</span><span class="n">side_effect</span><span class="o">=</span><span class="n">mock_fetch_weather</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="k">with</span> <span class="n">patch</span><span class="o">.</span><span class="n">object</span><span class="p">(</span><span class="n">app</span><span class="o">.</span><span class="n">main</span><span class="p">,</span> <span class="s1">&#39;fetch_weather&#39;</span><span class="p">,</span> <span class="n">new</span><span class="o">=</span><span class="n">mock</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">        <span class="k">yield</span>
</span></span></code></pre></div><p>In our test, we just need to again pass in the fixture as an argument.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/test_weather_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_weather1</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">mock_fetch_weather_factory</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/weather/atlanta&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;city&#39;</span><span class="p">:</span> <span class="s1">&#39;atlanta&#39;</span><span class="p">,</span> <span class="s1">&#39;weather&#39;</span><span class="p">:</span> <span class="s1">&#39;rainy&#39;</span><span class="p">}</span>
</span></span></code></pre></div><h3 id="creating-a-parametrized-fixture">Creating a parametrized fixture</h3>
<p>The above example probably isn&rsquo;t very useful in practice. In the real world, if we&rsquo;re testing multiple endpoints that
call the same function, we probably want to test different scenarios. For example, we may want to test that our
endpoint returns the correct response for different weather conditions. We could do this by creating multiple fixtures
that patch the function with different mock functions, but this is a lot of code duplication and basically defeats the
purpose of using a fixture at all. Instead, we can use a parametrized fixture to pass in values to our fixture to
make it behave differently depending on our test.</p>
<p>To do this, we&rsquo;re going to create another fixture, but this one will take a parameter. It essentially wraps our previous
factory function, but now we can pass in a parameter to the fixture.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">mock_fetch_weather_parametrized</span><span class="p">(</span><span class="n">request</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_fetch_weather_factory</span><span class="p">(</span><span class="n">weather</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">def</span> <span class="nf">mock_fetch_weather</span><span class="p">(</span><span class="o">*</span><span class="n">args</span><span class="p">,</span> <span class="o">**</span><span class="n">kwargs</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="k">return</span> <span class="n">weather</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">return</span> <span class="n">mock_fetch_weather</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">mock_weather</span> <span class="o">=</span> <span class="n">request</span><span class="o">.</span><span class="n">param</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">with</span> <span class="n">patch</span><span class="o">.</span><span class="n">object</span><span class="p">(</span><span class="n">app</span><span class="o">.</span><span class="n">main</span><span class="p">,</span> <span class="s1">&#39;fetch_weather&#39;</span><span class="p">,</span> <span class="n">new</span><span class="o">=</span><span class="n">mock_fetch_weather_factory</span><span class="p">(</span><span class="n">mock_weather</span><span class="p">)):</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">yield</span>
</span></span></code></pre></div><p>And then, when we call our test, we need to decorate it with <code>pytest.mark.parametrize</code>, and pass in the
fixture as an argument. I&rsquo;ve set up two tests here so we can really see how it works.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/test_weather_api.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">mark</span><span class="o">.</span><span class="n">parametrize</span><span class="p">(</span><span class="s1">&#39;mock_fetch_weather_parametrized&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;cloudy&#39;</span><span class="p">],</span> <span class="n">indirect</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_weather_parameterized</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">mock_fetch_weather_parametrized</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/weather/atlanta&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;city&#39;</span><span class="p">:</span> <span class="s1">&#39;atlanta&#39;</span><span class="p">,</span> <span class="s1">&#39;weather&#39;</span><span class="p">:</span> <span class="s1">&#39;cloudy&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">mark</span><span class="o">.</span><span class="n">parametrize</span><span class="p">(</span><span class="s1">&#39;mock_fetch_weather_parametrized&#39;</span><span class="p">,</span> <span class="p">[</span><span class="s1">&#39;tornados&#39;</span><span class="p">],</span> <span class="n">indirect</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_weather_parameterized</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">mock_fetch_weather_parametrized</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/weather/atlanta&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;city&#39;</span><span class="p">:</span> <span class="s1">&#39;atlanta&#39;</span><span class="p">,</span> <span class="s1">&#39;weather&#39;</span><span class="p">:</span> <span class="s1">&#39;tornados&#39;</span><span class="p">}</span>
</span></span></code></pre></div><p>The parameterized fixture is more complicated to set up, but it is incredibly useful in practice when you have to
simulate different responses from external APIs.</p>
<h2 id="mocking-mongodb">Mocking MongoDB</h2>
<p>Our FastAPI application interacts with MongoDB. During testing, we might not want to (or really be able to in some cases)
hit a real database with real/mocked data.  Instead, we acn mock MongoDB by using the <a href="https://github.com/mongomock/mongomock">mongomock</a> library, which
simulates a MongoDB client. (Note: In my experience, mongomock can be difficult to work with and some practice to get
working, but were going to proceed with it for now)</p>
<h3 id="creating-a-mock-mongodb-client">Creating a Mock MongoDB Client</h3>
<p>In our application, we employ context management for database interactions, utilizing Python&rsquo;s <code>with</code> statement. This
demands that the object used within the <code>with</code> statement must implement context management protocols, specifically
<code>__enter__</code> and <code>__exit__</code> methods.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: app/db.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nd">@contextmanager</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">get_mongodb</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">with</span> <span class="n">MongoClient</span><span class="p">()</span> <span class="k">as</span> <span class="n">client</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="n">db</span> <span class="o">=</span> <span class="n">client</span><span class="p">[</span><span class="s2">&#34;preferences&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="k">yield</span> <span class="n">db</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="nb">print</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;Error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">raise</span>
</span></span></code></pre></div><p>The mongomock library doesn&rsquo;t implement these methods, so to align with these requirements, we define a custom
MockMongoClient class to wrap our mongomock client. This class mimics the behavior of the actual
MongoClient by implementing the <code>__enter__</code> and <code>__exit__</code> methods. This ensures compatibility with the existing code
that expects a context-managed database client.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">class</span> <span class="nc">MockMongoClient</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">def</span> <span class="fm">__init__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">db</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="bp">self</span><span class="o">.</span><span class="n">db</span> <span class="o">=</span> <span class="n">db</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">def</span> <span class="fm">__enter__</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">return</span> <span class="bp">self</span><span class="o">.</span><span class="n">db</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="k">def</span> <span class="fm">__exit__</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="o">*</span><span class="n">args</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">pass</span>
</span></span></code></pre></div><p>Using our MockMongoClient class, we can now create our mock database client. We&rsquo;ll do this in a fixture so we can
reuse it in multiple tests.</p>
<h3 id="creating-a-mock-mongodb-client-fixtures">Creating a Mock MongoDB Client Fixtures</h3>
<p>I&rsquo;m going to initialize two separate fixtures here, one with an empty database and one with some data initialized. (In
theory, we should be able to set the scope of the fixture to <code>session</code> or <code>module</code> and just initialize the database
once, add data through other tests, and then use that data for testing later, but I haven&rsquo;t been able to get that
working with mongomock. The project seems to have been designed more with unit tests in mind and is not a complete
implementation or drop in replacement. If you know how to make this work, please let me know!)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">mock_mongodb</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_get_mongodb</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">mock_client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="k">return</span> <span class="n">MockMongoClient</span><span class="p">(</span><span class="n">mock_client</span><span class="o">.</span><span class="n">db</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="k">return</span> <span class="n">mock_get_mongodb</span>
</span></span></code></pre></div><p>and initialize some data&hellip;</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">def</span> <span class="nf">mock_mongodb_initialized</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_get_mongodb</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">mock_client</span> <span class="o">=</span> <span class="n">MongoClient</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">mock_client</span><span class="o">.</span><span class="n">db</span><span class="o">.</span><span class="n">preferences</span><span class="o">.</span><span class="n">insert_one</span><span class="p">({</span><span class="s2">&#34;username&#34;</span><span class="p">:</span> <span class="s2">&#34;alexjacobs&#34;</span><span class="p">,</span> <span class="s2">&#34;city&#34;</span><span class="p">:</span> <span class="s2">&#34;Berlin&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">return</span> <span class="n">MockMongoClient</span><span class="p">(</span><span class="n">mock_client</span><span class="o">.</span><span class="n">db</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">
</span></span><span class="line"><span class="ln">9</span><span class="cl">    <span class="k">return</span> <span class="n">mock_get_mongodb</span>
</span></span></code></pre></div><p>And now we can write our tests. We&rsquo;ll pass in our fixture and again use the <code>dependency_overrides</code> feature to inject
our
mock database client into our app (overriding the <code>get_mongodb</code> function)</p>
<h3 id="writing-tests-integration-tests-using-our-mocked-mongodb-client">Writing Tests Integration Tests Using Our Mocked MongoDB Client</h3>
<p>We&rsquo;ve got three simple tests here.<br>
First, we test that our endpoint correctly returns a 404 if no user preferences exist (we use our non-initialized
<code>mock_mongodb</code> fixture for this)</p>
<p>Next, we use our initialized <code>mock_mongodb</code> fixture to test that our endpoint correctly returns the user preferences.</p>
<p>Finally, we test that we can save user preferences (we use our non-initialized <code>mock_mongodb</code> fixture for this, but it
should work with either)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/user_preference_api.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">from</span> <span class="nn">app.main</span> <span class="kn">import</span> <span class="n">app</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">from</span> <span class="nn">app.fake_db</span> <span class="kn">import</span> <span class="n">get_mongodb</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_user_preferences_404</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">mock_mongodb</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="c1"># should return 404 since no user preferences exist</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="p">[</span><span class="n">get_mongodb</span><span class="p">]</span> <span class="o">=</span> <span class="n">mock_mongodb</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me/preferences&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">404</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;detail&#39;</span><span class="p">:</span> <span class="s1">&#39;Preferences not found&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_user_preferences_initialized</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">mock_mongodb_initialized</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="c1"># we&#39;re using our mock_mongodb_initialized fixture here which has our user preferences already set</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="p">[</span><span class="n">get_mongodb</span><span class="p">]</span> <span class="o">=</span> <span class="n">mock_mongodb_initialized</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me/preferences&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;city&#39;</span><span class="p">:</span> <span class="s1">&#39;Berlin&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="k">def</span> <span class="nf">test_post_user_preferences</span><span class="p">(</span><span class="n">client</span><span class="p">,</span> <span class="n">mock_mongodb</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="p">[</span><span class="n">get_mongodb</span><span class="p">]</span> <span class="o">=</span> <span class="n">mock_mongodb</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/users/me/preferences&#34;</span><span class="p">,</span> <span class="n">json</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;city&#34;</span><span class="p">:</span> <span class="s2">&#34;Berlin&#34;</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s2">&#34;detail&#34;</span><span class="p">:</span> <span class="s2">&#34;success&#34;</span><span class="p">}</span>
</span></span></code></pre></div><p>And that&rsquo;s it!  Pretty simple to write the actual tests once we have our mocked database client set up correctly.</p>
<h2 id="mocking-aws-s3">Mocking AWS S3</h2>
<p>Our FastAPI application interacts with AWS S3 for storing and retrieving user profile pictures. During testing, we don&rsquo;t
want to use a real S3 bucket because it would require us to manage real AWS resources. This would complicate our tests
and potentially incur costs. Therefore, we mock the S3 bucket. (The same patterns used here could be applied to other
AWS services)</p>
<h3 id="creating-the-mock-s3-fixture">Creating the Mock S3 Fixture</h3>
<p>We mock the S3 bucket by using the mock_s3 decorator from the moto library, which simulates an S3 bucket. This is done
in a fixture so we can reuse it in multiple tests.</p>
<p>(moto is a great library for mocking AWS services, and you&rsquo;ll see we are able to set our fixture scope to session,
allowing us to maintain bucket state across multiple tests)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span><span class="p">(</span><span class="n">scope</span><span class="o">=</span><span class="s2">&#34;session&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">mock_s3_bucket</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">with</span> <span class="n">mock_s3</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">conn</span> <span class="o">=</span> <span class="n">boto3</span><span class="o">.</span><span class="n">resource</span><span class="p">(</span><span class="s1">&#39;s3&#39;</span><span class="p">,</span> <span class="n">region_name</span><span class="o">=</span><span class="s1">&#39;us-east-1&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">conn</span><span class="o">.</span><span class="n">create_bucket</span><span class="p">(</span><span class="n">Bucket</span><span class="o">=</span><span class="n">bucket</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="c1"># we could upload a test file(s) here if we wanted to</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="c1"># s3_client = boto3.client(&#39;s3&#39;, region_name=&#39;us-east-1&#39;)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="c1"># s3_client.upload_file(&#39;tests/assets/duck.png&#39;, bucket, &#39;profile_pics/alexjacobs.png&#39;) </span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">yield</span>
</span></span></code></pre></div><h3 id="integrating-with-test-client">Integrating with Test Client</h3>
<p>This looks similar to our MockMongo fixtures, but this time, rather than pass our fixture into our tests, we pass it
into the client fixture.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># File: tests/conftest.py</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nd">@pytest</span><span class="o">.</span><span class="n">fixture</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">def</span> <span class="nf">client</span><span class="p">(</span><span class="n">mock_s3_bucket</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="c1"># we patch auth within our client fixture</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kn">from</span> <span class="nn">app.main</span> <span class="kn">import</span> <span class="n">app</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">def</span> <span class="nf">mock_get_auth</span><span class="p">():</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">return</span> <span class="n">TokenData</span><span class="p">(</span><span class="n">username</span><span class="o">=</span><span class="s2">&#34;alexjacobs&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="p">[</span><span class="n">get_auth</span><span class="p">]</span> <span class="o">=</span> <span class="n">mock_get_auth</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">with</span> <span class="n">TestClient</span><span class="p">(</span><span class="n">app</span><span class="p">)</span> <span class="k">as</span> <span class="n">test_client</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">yield</span> <span class="n">test_client</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="n">app</span><span class="o">.</span><span class="n">dependency_overrides</span><span class="o">.</span><span class="n">clear</span><span class="p">()</span>
</span></span></code></pre></div><h3 id="writing-the-tests">Writing the Tests</h3>
<p>Now we can write our tests. We&rsquo;ll start by testing that we get the right message if no profile picture exists for the
user</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests / test_user_preference_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_user_profile_pic_404</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me/profile_pic&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">404</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;detail&#39;</span><span class="p">:</span> <span class="s1">&#39;No profile pic found&#39;</span><span class="p">}</span>
</span></span></code></pre></div><p>Now we&rsquo;ll test adding one&hellip;</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/test_user_preference_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_set_user_profile_pic</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s1">&#39;tests/assets/duck.png&#39;</span><span class="p">,</span> <span class="s1">&#39;rb&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">post</span><span class="p">(</span><span class="s2">&#34;/users/me/profile_pic&#34;</span><span class="p">,</span> <span class="n">files</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;picture&#34;</span><span class="p">:</span> <span class="p">(</span><span class="s2">&#34;duck.png&#34;</span><span class="p">,</span> <span class="n">f</span><span class="p">,</span> <span class="s2">&#34;image/png&#34;</span><span class="p">)})</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()</span> <span class="o">==</span> <span class="p">{</span><span class="s1">&#39;detail&#39;</span><span class="p">:</span> <span class="s1">&#39;success&#39;</span><span class="p">}</span>
</span></span></code></pre></div><p>Finally, we can test getting one. (notice how this is using the same file we uploaded in the previous test, this is due
to the fact that we&rsquo;re using the session scope for our mock_s3_bucket fixture)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># File: tests/test_user_preference_api.py</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">def</span> <span class="nf">test_get_user_profile_pic</span><span class="p">(</span><span class="n">client</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">response</span> <span class="o">=</span> <span class="n">client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;/users/me/profile_pic&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span> <span class="o">==</span> <span class="mi">200</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s1">&#39;tests/assets/duck.png&#39;</span><span class="p">,</span> <span class="s1">&#39;rb&#39;</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">original_image_data</span> <span class="o">=</span> <span class="n">f</span><span class="o">.</span><span class="n">read</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="n">original_base64</span> <span class="o">=</span> <span class="n">base64</span><span class="o">.</span><span class="n">b64encode</span><span class="p">(</span><span class="n">original_image_data</span><span class="p">)</span><span class="o">.</span><span class="n">decode</span><span class="p">(</span><span class="s1">&#39;utf-8&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">
</span></span><span class="line"><span class="ln">9</span><span class="cl">    <span class="k">assert</span> <span class="n">response</span><span class="o">.</span><span class="n">json</span><span class="p">()[</span><span class="s1">&#39;image&#39;</span><span class="p">]</span> <span class="o">==</span> <span class="n">original_base64</span>
</span></span></code></pre></div><h2 id="wrapping-up">Wrapping Up</h2>
<p>This was a fairly deep dive.  We&rsquo;ve unraveled the intricacies of integration testing in FastAPI, which is not without
its challenges when it comes to mocking external dependencies. We&rsquo;ve gone through a
variety of techniques to mock authentication, from simple dependency_overrides to more advanced fixture-based
strategies. We&rsquo;ve also tackled how to mock external APIs using Python&rsquo;s unittest.mock.patch and pytest&rsquo;s parametrized
fixtures.</p>
<p>When it comes to databases, MongoDB adds another layer of complexity. We&rsquo;ve seen how Mongomock can be a useful tool,
albeit with its own set of limitations. We crafted custom mock MongoDB clients and fixtures to ease this pain point. As
for AWS S3, the Moto library proved to be a robust tool, enabling us to mock S3 buckets effectively, even allowing state
persistence across tests.</p>
<p>The aim has been to arm you with a set of tools and strategies for your FastAPI testing arsenal. Whether it&rsquo;s a
simple authentication mock or a more complex external service, you should now be equipped to tackle these head-on. Happy
testing.</p>
<p>Happy testing.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>CheeseGPT</title>
      <link>https://alex-jacobs.com/posts/cheesegpt/</link>
      <pubDate>Tue, 14 Mar 2023 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/cheesegpt/</guid>
      <description>A (Very) Simple RAG Tutorial</description>
      <content:encoded><![CDATA[<h2 id="introduction">Introduction</h2>
<p>This toy project was originally created for a guest lecture to a Data Science 101 course (and its quality may reflect that :)
This post extends that lecture, designed to provide a high-level understanding and example of <em>Retrieval Augmented Generation</em> (RAG).</p>
<p>We&rsquo;ll go through the steps of creating a RAG based LLM system, explaining what we&rsquo;re doing along the way, and why.
For the argument about why RAG works at all — and where it starts to break down architecturally — see
<a href="/posts/rag/">RAG: From Context Injection to Knowledge Integration</a>.</p>
<p>You can follow along with the slides and code <a href="https://github.com/alexjacobs08/cheeseGPT">here</a></p>
<p><img loading="lazy" src="/posts/cheesegpt/img1.png" type="" alt="cheeseGPT"  /></p>
<h3 id="the-cheesegpt-system">The CheeseGPT System</h3>
<p>CheeseGPT combines Large Language Models (LLMs) with the advanced capabilities
of Retrieval-Augmented Generation (RAG). At its core, CheeseGPT uses OpenAI&rsquo;s GPT-4 model for natural language processing.
This model serves as the backbone for generating human-like text responses. However, what sets CheeseGPT apart is its
integration with Langchain and a Redis database containing all of the information on Wikipedia relating to cheese.</p>
<p>When a user asks a question, the system utilizes RAG to retrieve the most relevant information/documents from its vector database, and then includes those in its message to the LLM.  This
allows the LLM to have specific and up-to-date information to use, extending from the data that it was trained on.</p>
<p>The image below, flow from right to left (steps 1-5) shows the high level design of this.  The user&rsquo;s query is passed into our embedding model.  We do a similarity search against our
database to retrieve the most relevant documents to our users question. And then these are included in context passed to our LLM.</p>
<p><img loading="lazy" src="/posts/cheesegpt/img2.png" type="" alt="rag_based_llm_design"  /></p>
<h6 id="httpswwwanyscalecombloga-comprehensive-guide-for-building-rag-based-llm-applications-part-1"><em><a href="https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1">https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1</a></em></h6>
<p>Below, we&rsquo;ll outline the steps to building this system.</p>
<p><strong>NOTE</strong>: this is an example, and probably doesn&rsquo;t make a ton of sense as a useful system.  (For one, we&rsquo;re getting our
data from wikipedia, which is already contained within the training data of GPT-4)  This is meant to be a high level
example that can show how a RAG based system can work, and to show what the possibilities are when integrating external
data with LLMs  (proprietary data, industry specific technical docs, etc.)</p>
<h2 id="data-collection-and-processing">Data Collection and Processing</h2>
<p>As with most projects, getting and munging your data is one of the most time consuming yet crucial elements.
For our CheeseGPT example, this involved scraping Wikipedia for
cheese-related articles, generating embeddings, and storing them in a Redis database. Below, I&rsquo;ll outline these steps
with code snippets for clarity.</p>
<h3 id="scraping-wikipedia">Scraping Wikipedia</h3>
<p>We start by extracting content from Wikipedia. We made a recursive function <code>get_page_content</code> to fetch pages related
to cheese, including summaries and sections.  (Note: This function could definitely be improved.)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl">
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="k">def</span> <span class="nf">get_page_content</span><span class="p">(</span><span class="n">page_title</span><span class="p">,</span> <span class="n">depth</span><span class="p">,</span> <span class="n">max_depth</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">wiki_wiki</span> <span class="o">=</span> <span class="n">wikipediaapi</span><span class="o">.</span><span class="n">Wikipedia</span><span class="p">(</span><span class="s1">&#39;MyCheeseRAGApp/1.0 (myemail@example.com)&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="k">if</span> <span class="n">depth</span> <span class="o">&gt;</span> <span class="n">max_depth</span> <span class="ow">or</span> <span class="n">page_title</span> <span class="ow">in</span> <span class="n">visited_pages</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">return</span> <span class="p">[],</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">visited_pages</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">page_title</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">visited_pages</span><span class="p">)</span> <span class="o">%</span> <span class="mi">10</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Visited pages: </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">visited_pages</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Fetching page &#39;</span><span class="si">{</span><span class="n">page_title</span><span class="si">}</span><span class="s2">&#39; (depth=</span><span class="si">{</span><span class="n">depth</span><span class="si">}</span><span class="s2">)&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="n">page</span> <span class="o">=</span> <span class="n">wiki_wiki</span><span class="o">.</span><span class="n">page</span><span class="p">(</span><span class="n">page_title</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">        <span class="k">if</span> <span class="ow">not</span> <span class="n">page</span><span class="o">.</span><span class="n">exists</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">            <span class="k">return</span> <span class="p">[],</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="n">texts</span><span class="p">,</span> <span class="n">metadata</span> <span class="o">=</span> <span class="p">[],</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">texts</span><span class="p">)</span> <span class="o">%</span> <span class="mi">100</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">            <span class="n">logger</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Texts length </span><span class="si">{</span><span class="nb">len</span><span class="p">(</span><span class="n">texts</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="c1"># Add page summary</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="n">texts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">page</span><span class="o">.</span><span class="n">summary</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="n">metadata</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s1">&#39;title&#39;</span><span class="p">:</span> <span class="n">page_title</span><span class="p">,</span> <span class="s1">&#39;section&#39;</span><span class="p">:</span> <span class="s1">&#39;Summary&#39;</span><span class="p">,</span> <span class="s1">&#39;url&#39;</span><span class="p">:</span> <span class="n">page</span><span class="o">.</span><span class="n">fullurl</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="c1"># Add sections</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="k">for</span> <span class="n">section</span> <span class="ow">in</span> <span class="n">page</span><span class="o">.</span><span class="n">sections</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="n">texts</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">section</span><span class="o">.</span><span class="n">text</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">            <span class="n">metadata</span><span class="o">.</span><span class="n">append</span><span class="p">({</span><span class="s1">&#39;title&#39;</span><span class="p">:</span> <span class="n">page_title</span><span class="p">,</span> <span class="s1">&#39;section&#39;</span><span class="p">:</span> <span class="n">section</span><span class="o">.</span><span class="n">title</span><span class="p">,</span> <span class="s1">&#39;url&#39;</span><span class="p">:</span> <span class="n">page</span><span class="o">.</span><span class="n">fullurl</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="c1"># Recursive fetching for links</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="k">if</span> <span class="n">depth</span> <span class="o">&lt;</span> <span class="n">max_depth</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">            <span class="k">for</span> <span class="n">link_title</span> <span class="ow">in</span> <span class="n">page</span><span class="o">.</span><span class="n">links</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">                <span class="n">link_texts</span><span class="p">,</span> <span class="n">link_metadata</span> <span class="o">=</span> <span class="n">get_page_content</span><span class="p">(</span><span class="n">link_title</span><span class="p">,</span> <span class="n">depth</span> <span class="o">+</span> <span class="mi">1</span><span class="p">,</span> <span class="n">max_depth</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">                <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">link_texts</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">                    <span class="n">texts</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">link_texts</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">                    <span class="n">metadata</span><span class="o">.</span><span class="n">extend</span><span class="p">(</span><span class="n">link_metadata</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="k">return</span> <span class="n">texts</span><span class="p">,</span> <span class="n">metadata</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">        <span class="n">logger</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;Error fetching page &#39;</span><span class="si">{</span><span class="n">page_title</span><span class="si">}</span><span class="s2">&#39;: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">        <span class="k">return</span> <span class="p">[],</span> <span class="p">[]</span>
</span></span></code></pre></div><p>This is a very greedy (and lazy) approach.  We don&rsquo;t discriminate at all, and we end up
with a ton of noise (things not related to cheese at all), but for our purposes of example, it works.</p>
<h3 id="generating-embeddings">Generating Embeddings</h3>
<p>Next, we need to generate our embeddings from our collected documents.</p>
<h4 id="what-are-embeddings">What are embeddings?</h4>
<p>Embeddings are high-dimensional, continuous vector representations of text, words, or other types of data,
where similar items have similar representations. They capture semantic relationships and features in a space where
operations like distance or angle measurement can indicate similarity or dissimilarity.</p>
<p>In machine learning, embeddings are used to convert categorical, symbolic, or textual data into a form that
algorithms can process more effectively, enabling tasks like natural language processing, recommendation systems,
and more sophisticated pattern recognition.</p>
<p>With our textual data collected, we&rsquo;ll be using OpenAI and Langchain to generate our embeddings. There are
lots of different ways to generate embeddings (plenty packages that run locally, too), but using OpenAI API to get them
is fast and easy for us. (and also dirt cheap)</p>
<p><strong>NOTE:</strong>
In a true production system, there would be <em>much</em> more consideration taken around generating embeddings.  This
is arguably the most important step in a RAG based system.  We&rsquo;d need to do experimentation with chunk size to see what
gives us the best results.  We&rsquo;d need to explore our vectors to make sure their working as expected, remove noise, etc.</p>
<h4 id="generating-embeddings-using-langchain-and-openai">Generating embeddings using LangChain and OpenAI</h4>
<p>Langchain makes it very easy to do create embeddings and store them in Redis without much thought,
but this step requires extreme care to generate good results in a production system</p>
<p>The snippet below takes our scraped wikipedia sections, generates embeddings for them using OpenAI&rsquo;s embeddings API,
and stores them in Redis.  Again, LangChain abstracts away a ton of complexity and makes this <em>really</em> easy for us.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">langchain.embeddings</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">rds</span> <span class="o">=</span> <span class="n">Redis</span><span class="o">.</span><span class="n">from_texts</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">texts</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">embeddings</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">metadatas</span><span class="o">=</span><span class="n">metadata</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">redis_url</span><span class="o">=</span><span class="s2">&#34;redis://localhost:6379&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="n">index_name</span><span class="o">=</span><span class="s2">&#34;cheese&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><h2 id="implementation-of-rag">Implementation of RAG</h2>
<p>Our RAG operates by creating an embedding of the user&rsquo;s question and then finding the most semantically similar documents
in our database (via cosine similarity between the embedding of our user&rsquo;s query and the N closest documents in our database).</p>
<p>We then include these documents / snippets in our request to the LLM, telling it that they are the most relevant documents based
on a similarity search.  The LLM can then use these documents as reference when generating its response.</p>
<p>Here&rsquo;s a simplified overview of the process with code snippets:</p>
<h3 id="embedding-user-queries">Embedding User Queries</h3>
<p>The user&rsquo;s query is converted into an embedding using the OpenAI API. This embedding represents the semantic content of
the query in a format that can be compared against the pre-computed embeddings of the database articles.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">from</span> <span class="nn">langchain.embeddings</span> <span class="kn">import</span> <span class="n">OpenAIEmbeddings</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">embeddings</span> <span class="o">=</span> <span class="n">OpenAIEmbeddings</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">query_embedding</span> <span class="o">=</span> <span class="n">embeddings</span><span class="o">.</span><span class="n">embed_text</span><span class="p">(</span><span class="n">user_query</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="retrieving-related-articles">Retrieving Related Articles</h3>
<p>We then use the query embedding to perform a similarity search in the Redis database. It retrieves a set number
of articles that are most semantically similar to the query.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">get_related_articles</span><span class="p">(</span><span class="n">query_embedding</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">3</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="k">return</span> <span class="n">rds</span><span class="o">.</span><span class="n">similarity_search</span><span class="p">(</span><span class="n">query_embedding</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">k</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="integrating-retrieved-data-into-gpt-4-prompts">Integrating Retrieved Data into GPT-4 Prompts</h3>
<p>The retrieved articles are formatted and integrated into the prompt for GPT-4. This allows GPT-4 to use the information
from these articles to generate a response that is not only contextually relevant but also rich in content.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">create_prompt_with_articles</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">articles</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">article_summaries</span> <span class="o">=</span> <span class="p">[</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">article</span><span class="p">[</span><span class="s1">&#39;title&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">: </span><span class="si">{</span><span class="n">article</span><span class="p">[</span><span class="s1">&#39;summary&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">&#34;</span> <span class="k">for</span> <span class="n">article</span> <span class="ow">in</span> <span class="n">articles</span><span class="p">]</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="sa">f</span><span class="s2">&#34;Question: </span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="se">\n\n</span><span class="s2">Related Information:</span><span class="se">\n</span><span class="s2">&#34;</span> <span class="o">+</span> <span class="s2">&#34;</span><span class="se">\n</span><span class="s2">&#34;</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="n">article_summaries</span><span class="p">)</span>
</span></span></code></pre></div><h3 id="generating-the-response">Generating the Response</h3>
<p>Finally, the enriched prompt is fed to GPT-4, which generates a response based on both the user&rsquo;s query and the
additional context provided by the retrieved articles.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">openai</span><span class="o">.</span><span class="n">ChatCompletion</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">messages</span><span class="o">=</span><span class="p">[{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">create_prompt_with_articles</span><span class="p">(</span><span class="n">user_query</span><span class="p">,</span> <span class="n">related_articles</span><span class="p">)}]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">)</span>
</span></span></code></pre></div><p>Through this process, CheeseGPT effectively combines the generative power of GPT-4 with the information retrieval
capabilities of RAG, resulting in responses that are informative, accurate, and contextually rich.</p>
<h2 id="the-chat-interface">The Chat Interface</h2>
<p>CheeseGPT&rsquo;s chat interface is an important component, orchestrating the interaction between the user, the
retrieval-augmented generation system, and the underlying Large Language Model (LLM).</p>
<p>For the purposes of our example, we have built the bindings for the interface, but did not create a fully interactive interface.</p>
<p>Let&rsquo;s dive into the key functions that make this interaction possible.</p>
<h3 id="connecting-to-redis">Connecting to Redis</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">rds_connect</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">rds</span> <span class="o">=</span> <span class="n">Redis</span><span class="o">.</span><span class="n">from_existing_index</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="n">embeddings</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="n">redis_url</span><span class="o">=</span><span class="s2">&#34;redis://localhost:6379&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="n">index_name</span><span class="o">=</span><span class="s2">&#34;cheese&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">        <span class="n">schema</span><span class="o">=</span><span class="s2">&#34;redis_schema.yaml&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="k">return</span> <span class="n">rds</span>
</span></span></code></pre></div><p>This function establishes a connection to the Redis database, where the precomputed embeddings of cheese-related
Wikipedia pages are stored.</p>
<h3 id="applying-filters">Applying Filters</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">get_filters</span><span class="p">():</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">is_not_external_link</span> <span class="o">=</span> <span class="n">RedisFilter</span><span class="o">.</span><span class="n">text</span><span class="p">(</span><span class="s2">&#34;section&#34;</span><span class="p">)</span> <span class="o">!=</span> <span class="s1">&#39;External Links&#39;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">is_not_see_also</span> <span class="o">=</span> <span class="n">RedisFilter</span><span class="o">.</span><span class="n">text</span><span class="p">(</span><span class="s2">&#34;section&#34;</span><span class="p">)</span> <span class="o">!=</span> <span class="s1">&#39;See Also&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">_filter</span> <span class="o">=</span> <span class="n">is_not_external_link</span> <span class="o">&amp;</span> <span class="n">is_not_see_also</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="k">return</span> <span class="n">_filter</span>
</span></span></code></pre></div><p>Filters are applied to ensure that irrelevant sections like &lsquo;External Links&rsquo; and &lsquo;See Also&rsquo; are excluded from the search
results.</p>
<h3 id="deduplicating-results">Deduplicating Results</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">dedupe_results</span><span class="p">(</span><span class="n">results</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">seen</span> <span class="o">=</span> <span class="nb">set</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">deduped_results</span> <span class="o">=</span> <span class="p">[]</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">for</span> <span class="n">result</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">        <span class="k">if</span> <span class="n">result</span><span class="o">.</span><span class="n">page_content</span> <span class="ow">not</span> <span class="ow">in</span> <span class="n">seen</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">            <span class="n">deduped_results</span><span class="o">.</span><span class="n">append</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">            <span class="n">seen</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">page_content</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="k">return</span> <span class="n">deduped_results</span>
</span></span></code></pre></div><p>This function ensures that duplicate content from the search results is removed, enhancing the quality of the final
output.  (This is necessary in our case because we were greedy / lazy when pulling our data / generating our vectors)</p>
<h3 id="retrieving-document-results">Retrieving Document Results</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">get_results</span><span class="p">(</span><span class="n">rds</span><span class="p">,</span> <span class="n">question</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">3</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">_filters</span> <span class="o">=</span> <span class="n">get_filters</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="n">dedupe_results</span><span class="p">(</span><span class="n">rds</span><span class="o">.</span><span class="n">similarity_search</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="n">k</span><span class="p">,</span> <span class="nb">filter</span><span class="o">=</span><span class="n">_filters</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="k">return</span> <span class="n">results</span>
</span></span></code></pre></div><p>This key function performs a similarity search in the Redis database using the user&rsquo;s query, filtered and deduplicated.</p>
<h3 id="formatting-rag-results">Formatting RAG Results</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">format_rag_results</span><span class="p">(</span><span class="n">rag_results</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">divider</span> <span class="o">=</span> <span class="s2">&#34;*********************RESULT*********************</span><span class="se">\n</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="k">return</span> <span class="p">[</span><span class="sa">f</span><span class="s2">&#34;</span><span class="si">{</span><span class="n">divider</span><span class="si">}</span><span class="s2"> </span><span class="si">{</span><span class="n">result</span><span class="o">.</span><span class="n">page_content</span><span class="si">}</span><span class="s2"> (</span><span class="si">{</span><span class="n">result</span><span class="o">.</span><span class="n">metadata</span><span class="p">[</span><span class="s1">&#39;url&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">result</span><span class="o">.</span><span class="n">metadata</span><span class="p">[</span><span class="s1">&#39;section&#39;</span><span class="p">]</span><span class="si">}</span><span class="s2">)&#34;</span> <span class="k">for</span> <span class="n">result</span> <span class="ow">in</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">            <span class="n">rag_results</span><span class="p">]</span>
</span></span></code></pre></div><p>The function formats the search results, making them readable and including the source information for transparency.</p>
<p>This is what our message looks like when we send it to GPT-4.  Our system prompt is first and includes instructions for the
model to use the retrieved documents when answering the question.</p>
<p>In our user message, you can see the user&rsquo;s question, and then the documents we retrieved, presented as a list with some formatting.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="cl"><span class="p">[</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;You are cheeseGPT, a retrieval augmented chatbot with expert knowledge of cheese. You are here to answer questions about cheese, and you should, when possible, cite your sources with the documents provided to you.&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">},</span>
</span></span><span class="line"><span class="cl">   <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="nt">&#34;content&#34;</span><span class="p">:</span> <span class="s2">&#34;User question: what is the biggest cheese sporting event.  Retrieved documents: [&#39;*********************RESULT*********************\\n The Lucerne Cheese Festival (German: Käsefest Luzern) is a cheese festival held annually in Lucerne, Switzerland. It was established in 2001 and is normally run on a weekend in the middle of October at the Kapellplatz (Chapel Square) in the city centre. The next festival is planned to take place on 14 October 2023.The event features the biggest cheese market in central Switzerland, and offers the greatest selection of cheeses. As well as the cheese market and live demonstrations of cheesemaking, typical events during the festival include a milking competition and music such as the Swiss alphorn.The 2012 event featured over 200 varieties of cheese over 23 market stalls, including goat and sheep cheese. The 2020 event was almost cancelled because of social distancing restrictions during the COVID-19 pandemic, but was approved a few days before with a strict requirement to wear masks. Instead of the Kapellplatz, the festival was run from the nearby Kurplatz (Spa Square). 288 variety of cheeses were available at the festival, including cheesemakers from outside the local region such as the Bernese Jura and Ticino, who had their own festivals cancelled. Around 5,800 people attended the festival, lower than the previous year, with around two-thirds fewer sales. The following year\\&#39;s event continued restrictions, where customers had to taste and buy cheese at a distance, though masks were no longer mandatory. The 2022 event featured demonstrations of the cheese making process, a chalet built of Swiss cheese, and a \&#34;cheese chalet\&#34; hosting cheese fondue and raclette.India Times in 2014 called it out as one of 10 world food festivals for foodies. (https://en.wikipedia.org/wiki/Lucerne_Cheese_Festival#Summary)&#39;, &#39;*********************RESULT*********************\\n In addition to sampling and purchasing more than 4,600 cheeses in the Cheese Pavilion, visitors to the show are treated to various attractions throughout the day including cheese making demonstrations, trophy presentations and live cookery demonstrations. (https://en.wikipedia.org/wiki/International_Cheese_Awards#Show features)&#39;, &#39;*********************RESULT*********************\\n The first annual event was held in 2000 in Oxfordshire, and was founded by Juliet Harbutt. Each year it is preceded by the British Cheese Awards, a ceremony which Harbutt created in 1994, judged by food experts and farmers, in which the best cheeses are awarded bronze, silver and gold medals.\\nAll cheeses are tasted blind, and the winners can then display their awards during the public-attended festival.  There are usually a variety of events at the festival such as seminars, masterclasses, and cheesemaking demonstrations.The event moved to Cheltenham in Gloucestershire in 2005. In 2006 the Sunday of the weekend was cancelled at great cost after the venue experienced flooding, and the decision was made to return to Oxfordshire in 2007.Early in 2008 the festival was sold to Cardiff Council; subsequently the event has been held in the grounds of Cardiff Castle in 2008, 2009, 2010, and 2011.The 2012 Great British Cheese Festival was held at Cardiff Castle on Saturday, September 22, and Sunday, September 23.\\nIn 2015, Harbutt returned to her native New Zealand. (https://en.wikipedia.org/wiki/The_Great_British_Cheese_Festival#History)&#39;]&#34;</span>
</span></span><span class="line"><span class="cl">   <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">]</span>
</span></span></code></pre></div><h3 id="generating-messages-for-the-llm">Generating Messages for the LLM</h3>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="k">def</span> <span class="nf">get_messages</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">rag_results</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">    <span class="n">messages</span> <span class="o">=</span> <span class="p">[</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;system&#34;</span><span class="p">,</span> <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="n">system_prompt</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;role&#34;</span><span class="p">:</span> <span class="s2">&#34;user&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">         <span class="s2">&#34;content&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;User question: </span><span class="si">{</span><span class="n">question</span><span class="si">}</span><span class="s2">.  Retrieved documents: </span><span class="si">{</span><span class="n">format_rag_results</span><span class="p">(</span><span class="n">rag_results</span><span class="p">)</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="p">]</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="k">return</span> <span class="n">messages</span>
</span></span></code></pre></div><p>This function prepares the input for the LLM, combining the system prompt, user question, and the retrieved documents.</p>
<p>The integration of these functions creates a seamless flow from the user&rsquo;s question to the LLM&rsquo;s informed response,
enabling CheeseGPT to provide expert-level insights into the world of cheese.</p>
<p>Putting it all together might look something like&hellip;</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="n">question</span> <span class="o">=</span> <span class="s2">&#34;what is the biggest cheese sporting event&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="n">get_results</span><span class="p">(</span><span class="n">rds</span><span class="p">,</span> <span class="n">question</span><span class="p">,</span> <span class="n">k</span><span class="o">=</span><span class="mi">3</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">messages</span> <span class="o">=</span> <span class="n">get_messages</span><span class="p">(</span><span class="n">question</span><span class="p">,</span> <span class="n">results</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">messages</span><span class="p">,</span> <span class="n">indent</span><span class="o">=</span><span class="mi">4</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">openai</span><span class="o">.</span><span class="n">ChatCompletion</span><span class="o">.</span><span class="n">create</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">model</span><span class="o">=</span><span class="s2">&#34;gpt-4&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">messages</span><span class="o">=</span><span class="n">messages</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="p">)</span>
</span></span></code></pre></div><h2 id="outcomes">Outcomes</h2>
<p>So, let&rsquo;s compare a question using our system vs. asking ChatGPT.  We&rsquo;ll use the same question above.</p>
<p>Using our system, we get this response:</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">The Lucerne Cheese Festival in Switzerland and the International Cheese Awards are two of the biggest cheese sporting events. 
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">The Lucerne Cheese Festival, a yearly event in Lucerne, Switzerland, features the largest cheese market in central Switzerland 
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">and provides a vast selection of cheeses. The festival also includes live cheesemaking demonstrations, a milking competition, 
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">and music. In past years, it has presented over 200 varieties of cheese over 23 market stalls, including goat and sheep cheese. 
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">The festival attracted around 5,800 attendees in 2020, despite social distancing restrictions. The India Times recognized it 
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">as one of 10 world food festivals for foodies in 2014 [^1^].
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">On the other hand, the International Cheese Awards allow visitors 
</span></span><span class="line"><span class="ln">10</span><span class="cl">to sample and purchase over 4,600 different cheeses in the Cheese Pavilion. The show features various attractions such as
</span></span><span class="line"><span class="ln">11</span><span class="cl">cheese making demonstrations, trophy presentations, and live cookery demonstrations [^2^].
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">The Great British Cheese Festival, although not as large as the other two events, is also a notable cheese event. It 
</span></span><span class="line"><span class="ln">14</span><span class="cl">hosts the British Cheese Awards, where the best cheeses are awarded bronze, silver, and gold medals, judged by food 
</span></span><span class="line"><span class="ln">15</span><span class="cl">experts and farmers. All cheeses are tasted blindly, and the winners can display their awards during the festival. 
</span></span><span class="line"><span class="ln">16</span><span class="cl">The festival typically includes seminars, masterclasses, and cheesemaking demonstrations [^3^].
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl">In conclusion, both the Lucerne Cheese Festival and the International Cheese Awards are considered some of the biggest 
</span></span><span class="line"><span class="ln">19</span><span class="cl">cheese sporting events worldwide.
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">[^1^]: (https://en.wikipedia.org/wiki/Lucerne_Cheese_Festival#Summary)
</span></span><span class="line"><span class="ln">22</span><span class="cl">[^2^]: (https://en.wikipedia.org/wiki/International_Cheese_Awards#Show_features)
</span></span><span class="line"><span class="ln">23</span><span class="cl">[^3^]: (https://en.wikipedia.org/wiki/The_Great_British_Cheese_Festival#History)&#34;
</span></span></code></pre></div><p>And if we ask ChatGPT the same question&hellip;</p>
<p><img loading="lazy" src="/posts/cheesegpt/img3.jpg" type="" alt="img3.jpg"  /></p>
<p>I&rsquo;m not sure which of these answer is <em>more</em> correct, and it doesn&rsquo;t matter for the purposes of this example.  The point is that we were
able to retrieve and include our own information, external to the model, and make it use that information in it&rsquo;s response.<br>
It&rsquo;s clear how many amazing use cases there are for something like this!</p>
<p>Hopefully this high level toy example was able to shed some light on what a RAG based system may look like.  Checkout the
additional resources linked before for more in-depth information. Thanks for reading!</p>
<h3 id="related-reading">Related reading</h3>
<ul>
<li><a href="/posts/rag/">RAG: From Context Injection to Knowledge Integration</a> — why this pattern works, and the architectural limits it runs into.</li>
<li><a href="/posts/the-case-against-pgvector/">The Case Against pgvector</a> — this toy used an in-memory vector store; here&rsquo;s what changes when you put one in production.</li>
<li><a href="/posts/practicalaifeatures/">A Production Framework for LLM Feature Evaluation</a> — deciding which LLM features are worth building at all.</li>
</ul>
<h3 id="additional-resources">Additional resources</h3>
<p><a href="https://github.com/ray-project/llm-applications/blob/main/notebooks/rag.ipynb">https://github.com/ray-project/llm-applications/blob/main/notebooks/rag.ipynb</a>
<a href="https://github.com/pchunduri6/rag-demystified">https://github.com/pchunduri6/rag-demystified</a>
<a href="https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1">https://www.anyscale.com/blog/a-comprehensive-guide-for-building-rag-based-llm-applications-part-1</a></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Effective Error Handling</title>
      <link>https://alex-jacobs.com/posts/errorhandling/</link>
      <pubDate>Wed, 14 Dec 2022 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/errorhandling/</guid>
      <description>A simple an effective approach for handling end users who struggle with errors</description>
      <content:encoded><![CDATA[<p>All too frequently, we, as developers, are faced with the task of handling errors. By developing effective messaging
for our users, we can make their experience with our software much more pleasant. While the task of crafting these
messages can seem mundane, it&rsquo;s a necessary step in making our software more user-friendly and intuitive.</p>
<p>And yet, all too frequently, end users will go out of their way to completely avoid even attempting to read the error message.</p>
<p>And after a long hard day writing bug free code and solving customer problems, there is nothing less satisfying than a
story that contains a screenshot of an error message that explains exactly the action the user needs to take the resolve the mistake
they have made.</p>
<p>Below we present a simple and effective approach to handling errors in client-side fleshware.</p>
<h1 id="an-error-message-that-requires-interaction">An error message that requires interaction</h1>
<p>If it&rsquo;s not yet clear, I&rsquo;ll take this opportunity to say that this is (mostly) a joke.</p>
<p><style>
    .modal {
        display: none;
        position: fixed;
        z-index: 1;
        left: 0;
        top: 0;
        width: 100%;
        height: 100%;
        overflow: auto;
        background-color: rgb(0, 0, 0);
        background-color: rgba(0, 0, 0, 0.5);

    }

    .modal-content {
        display: block;
        background-color: #fefefe;
        margin: 25% auto;
        padding: 20px;
        border: 3px solid #dc3545;
        width: 60%;
        height: auto;
        opacity: 1;
        border-radius: 25px;
        text-align: center;
    }

    .modal input {
        background-color: #ffffff;
        border: 1px solid #dc3545;
        border-radius: 5px;
        width: 50%;

    }

    .error-button {
        background-color: #dc3545;
        border: 1px solid #dc3545;
        border-radius: 5px;
        width: 40%;
        color: #ffffff;
        font-weight: bold;
        display: grid;
        margin: 0 auto;
    }

</style>
<div id="errorModal" class="modal">

    <div class="modal-content">
        <div style="color: #dc3545;" id='errorMessage'></div>
        <br>
        <p style="color: gray; opacity: 65%">
            type the error message to confirm you've seen it and disable the error modal
        </p>
        <input type="text" id="textinput" name="fname">
    </div>

</div>
<div class="error-button">
    <button type="submit" onclick="errorMessage('There is an error due to the way you did it')">
        Click here to display error
    </button>
</div>
<script>
    function errorMessage(displayErrorMessage) {
        let errorModal = document.getElementById("errorModal");
        let errorMessage = document.getElementById('errorMessage')
        errorMessage.textContent = displayErrorMessage
        errorMessage.style.fontWeight = 'bold';
        errorModal.style.display = "block";

        let inputBox = document.getElementById('textinput');

        inputBox.onkeyup = function () {
            var text = inputBox.value
            if (displayErrorMessage.startsWith(text)) {
                errorMessage.innerHTML = "<span style='color: " + 'black' + ";'>" + text + "</span>" +
                    "<span style='color: " + '#dc3545' + ";'>" +
                    displayErrorMessage.replace(text, '') + "</span>"
            }

            if (text === displayErrorMessage) {
                errorModal.style.display = "none";
                inputBox.value = '';
            }
        }
        console.log(errorMessage)
    }
</script>
<br>
<br>
And simple as that, we reduce the number of bug reports we receive by 100%!</p>
<p>I&rsquo;m certainly no UI designer, but with a little love, I expect this to be a core product feature in all modern softwares
going forward.</p>
<p>Checkout the fiddle below to make your own improvements!
<a href="https://jsfiddle.net/eg142dkp/2/">https://jsfiddle.net/eg142dkp/2/</a></p>
<h3 id="wondering-how-i-made-this">Wondering how I made this?</h3>
<p>First I had to learn javascript.  Then I had to learn CSS and HTML.  After all that,
I had to learn about Hugo and <a href="https://gohugo.io/content-management/shortcodes/">Shortcodes</a></p>
<p>By that point, I was a certified full-stack developer, and I was able to create the following shortcode to render the modal</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-html" data-lang="html"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="p">&lt;</span><span class="nt">style</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="p">.</span><span class="nc">modal</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="k">display</span><span class="p">:</span> <span class="kc">none</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="k">position</span><span class="p">:</span> <span class="kc">fixed</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="k">z-index</span><span class="p">:</span> <span class="mi">1</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="k">left</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="k">top</span><span class="p">:</span> <span class="mi">0</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="k">width</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="k">height</span><span class="p">:</span> <span class="mi">100</span><span class="kt">%</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="k">overflow</span><span class="p">:</span> <span class="kc">auto</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="k">background-color</span><span class="p">:</span> <span class="nb">rgb</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">);</span> 
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">background-color</span><span class="p">:</span> <span class="nb">rgba</span><span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mf">0.5</span><span class="p">);</span> 
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">.</span><span class="nc">modal-content</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="k">display</span><span class="p">:</span> <span class="kc">block</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="k">background-color</span><span class="p">:</span> <span class="mh">#fefefe</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="k">margin</span><span class="p">:</span> <span class="mi">25</span><span class="kt">%</span> <span class="kc">auto</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="k">padding</span><span class="p">:</span> <span class="mi">20</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">        <span class="k">border</span><span class="p">:</span> <span class="mi">3</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#dc3545</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">        <span class="k">width</span><span class="p">:</span> <span class="mi">60</span><span class="kt">%</span><span class="p">;</span> 
</span></span><span class="line"><span class="ln">23</span><span class="cl">        <span class="k">height</span><span class="p">:</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="k">opacity</span><span class="p">:</span> <span class="mi">1</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="k">border-radius</span><span class="p">:</span> <span class="mi">25</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">        <span class="k">text-align</span><span class="p">:</span> <span class="kc">center</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="p">.</span><span class="nc">modal</span> <span class="nt">input</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">        <span class="k">background-color</span><span class="p">:</span> <span class="mh">#ffffff</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">        <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#dc3545</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">        <span class="k">border-radius</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">        <span class="k">width</span><span class="p">:</span> <span class="mi">50</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">
</span></span><span class="line"><span class="ln">35</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">
</span></span><span class="line"><span class="ln">37</span><span class="cl">    <span class="p">.</span><span class="nc">error-button</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="k">background-color</span><span class="p">:</span> <span class="mh">#dc3545</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">        <span class="k">border</span><span class="p">:</span> <span class="mi">1</span><span class="kt">px</span> <span class="kc">solid</span> <span class="mh">#dc3545</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">        <span class="k">border-radius</span><span class="p">:</span> <span class="mi">5</span><span class="kt">px</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">41</span><span class="cl">        <span class="k">width</span><span class="p">:</span> <span class="mi">40</span><span class="kt">%</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">        <span class="k">color</span><span class="p">:</span> <span class="mh">#ffffff</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">        <span class="k">font-weight</span><span class="p">:</span> <span class="kc">bold</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">44</span><span class="cl">        <span class="k">display</span><span class="p">:</span> <span class="k">grid</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">45</span><span class="cl">        <span class="k">margin</span><span class="p">:</span> <span class="mi">0</span> <span class="kc">auto</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">
</span></span><span class="line"><span class="ln">48</span><span class="cl"><span class="p">&lt;/</span><span class="nt">style</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;errorModal&#34;</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;modal&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">
</span></span><span class="line"><span class="ln">51</span><span class="cl">    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;modal-content&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">        <span class="p">&lt;</span><span class="nt">div</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;color: #dc3545;&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#39;errorMessage&#39;</span><span class="p">&gt;&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">53</span><span class="cl">        <span class="p">&lt;</span><span class="nt">br</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">54</span><span class="cl">        <span class="p">&lt;</span><span class="nt">p</span> <span class="na">style</span><span class="o">=</span><span class="s">&#34;color: gray; opacity: 65%&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">            type the error message to confirm you&#39;ve seen it and disable the error modal
</span></span><span class="line"><span class="ln">56</span><span class="cl">        <span class="p">&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">        <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;textinput&#34;</span> <span class="na">name</span><span class="o">=</span><span class="s">&#34;fname&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">
</span></span><span class="line"><span class="ln">60</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;error-button&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">    <span class="p">&lt;</span><span class="nt">button</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;submit&#34;</span> <span class="na">onclick</span><span class="o">=</span><span class="s">&#34;errorMessage(&#39;There is an error due to the way you did it&#39;)&#34;</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">        Click here to display error
</span></span><span class="line"><span class="ln">64</span><span class="cl">    <span class="p">&lt;/</span><span class="nt">button</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl"><span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl"><span class="p">&lt;</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl">    <span class="kd">function</span> <span class="nx">errorMessage</span><span class="p">(</span><span class="nx">displayErrorMessage</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl">        <span class="kd">let</span> <span class="nx">errorModal</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">&#34;errorModal&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">69</span><span class="cl">        <span class="kd">let</span> <span class="nx">errorMessage</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;errorMessage&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">70</span><span class="cl">        <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="nx">displayErrorMessage</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl">        <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">fontWeight</span> <span class="o">=</span> <span class="s1">&#39;bold&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl">        <span class="nx">errorModal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">&#34;block&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">73</span><span class="cl">
</span></span><span class="line"><span class="ln">74</span><span class="cl">        <span class="kd">let</span> <span class="nx">inputBox</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s1">&#39;textinput&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">75</span><span class="cl">
</span></span><span class="line"><span class="ln">76</span><span class="cl">        <span class="nx">inputBox</span><span class="p">.</span><span class="nx">onkeyup</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">77</span><span class="cl">            <span class="kd">var</span> <span class="nx">text</span> <span class="o">=</span> <span class="nx">inputBox</span><span class="p">.</span><span class="nx">value</span>
</span></span><span class="line"><span class="ln">78</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">displayErrorMessage</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="nx">text</span><span class="p">))</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">79</span><span class="cl">                <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s2">&#34;&lt;span style=&#39;color: &#34;</span> <span class="o">+</span> <span class="s1">&#39;black&#39;</span> <span class="o">+</span> <span class="s2">&#34;;&#39;&gt;&#34;</span> <span class="o">+</span> <span class="nx">text</span> <span class="o">+</span> <span class="s2">&#34;&lt;/span&gt;&#34;</span> <span class="o">+</span>
</span></span><span class="line"><span class="ln">80</span><span class="cl">                    <span class="s2">&#34;&lt;span style=&#39;color: &#34;</span> <span class="o">+</span> <span class="s1">&#39;#dc3545&#39;</span> <span class="o">+</span> <span class="s2">&#34;;&#39;&gt;&#34;</span> <span class="o">+</span>
</span></span><span class="line"><span class="ln">81</span><span class="cl">                    <span class="nx">displayErrorMessage</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">text</span><span class="p">,</span> <span class="s1">&#39;&#39;</span><span class="p">)</span> <span class="o">+</span> <span class="s2">&#34;&lt;/span&gt;&#34;</span>
</span></span><span class="line"><span class="ln">82</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">83</span><span class="cl">
</span></span><span class="line"><span class="ln">84</span><span class="cl">            <span class="k">if</span> <span class="p">(</span><span class="nx">text</span> <span class="o">===</span> <span class="nx">displayErrorMessage</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">85</span><span class="cl">                <span class="nx">errorModal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">&#34;none&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">86</span><span class="cl">                <span class="nx">inputBox</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s1">&#39;&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">87</span><span class="cl">            <span class="p">}</span>
</span></span><span class="line"><span class="ln">88</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">89</span><span class="cl">        <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">errorMessage</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">90</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">91</span><span class="cl"><span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</span></span></code></pre></div><p>and it goes right here in my <code>layouts/shortcodes</code> directory</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">.
</span></span><span class="line"><span class="ln">2</span><span class="cl">└── layouts
</span></span><span class="line"><span class="ln">3</span><span class="cl">    └── shortcodes
</span></span><span class="line"><span class="ln">4</span><span class="cl">        └── error-modal.html
</span></span></code></pre></div><p>and then in my markdown file, I can just call it like this</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="ln">1</span><span class="cl">{{&lt; error-model &gt;}}
</span></span></code></pre></div><p><br>
Hugo is neat</p>
<h1 id="update-12-27-22----openai">Update 12-27-22 &ndash; OpenAI</h1>
<p>It is worth updating this post to say that I gave this prompt to the openai <code>code-davinci-002</code> model</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="cm">/* create a button that says &#39;click to display error message&#39;. 
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="cm">when the button is clicked, a modal should pop up that has an error message and a text box. 
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="cm">the modal should not have an x to close it. the only way to close the model is to type the error message into the text box */</span>
</span></span></code></pre></div><p>and within seconds had a fully working implementation</p>
<p>Checkout openAI&rsquo;s fiddle for comparison.  Maybe we&rsquo;re all wasting our time</p>
<p><a href="https://jsfiddle.net/Lkyxuc0v/">https://jsfiddle.net/Lkyxuc0v/</a></p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">var</span> <span class="nx">button</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;button&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="nx">button</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s1">&#39;click to display error message&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">button</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="nx">button</span><span class="p">.</span><span class="nx">onclick</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="kd">var</span> <span class="nx">modal</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">position</span> <span class="o">=</span> <span class="s1">&#39;fixed&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">top</span> <span class="o">=</span> <span class="s1">&#39;0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">left</span> <span class="o">=</span> <span class="s1">&#39;0&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="s1">&#39;100%&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="s1">&#39;100%&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">backgroundColor</span> <span class="o">=</span> <span class="s1">&#39;rgba(0,0,0,0.5)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">zIndex</span> <span class="o">=</span> <span class="s1">&#39;9999&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">modal</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="kd">var</span> <span class="nx">errorMessage</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="s1">&#39;Error: You must type the error message into the text box to close this modal.&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">position</span> <span class="o">=</span> <span class="s1">&#39;absolute&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">top</span> <span class="o">=</span> <span class="s1">&#39;50%&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">left</span> <span class="o">=</span> <span class="s1">&#39;50%&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">transform</span> <span class="o">=</span> <span class="s1">&#39;translate(-50%, -50%)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">backgroundColor</span> <span class="o">=</span> <span class="s1">&#39;white&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">padding</span> <span class="o">=</span> <span class="s1">&#39;20px&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">    <span class="nx">errorMessage</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">border</span> <span class="o">=</span> <span class="s1">&#39;1px solid black&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">errorMessage</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="kd">var</span> <span class="nx">textBox</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;input&#39;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">type</span> <span class="o">=</span> <span class="s1">&#39;text&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">position</span> <span class="o">=</span> <span class="s1">&#39;absolute&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">top</span> <span class="o">=</span> <span class="s1">&#39;60%&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">left</span> <span class="o">=</span> <span class="s1">&#39;50%&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">transform</span> <span class="o">=</span> <span class="s1">&#39;translate(-50%, -50%)&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="s1">&#39;200px&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="s1">&#39;20px&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">border</span> <span class="o">=</span> <span class="s1">&#39;1px solid black&#39;</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">    <span class="nx">modal</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">textBox</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">    <span class="nx">textBox</span><span class="p">.</span><span class="nx">onkeyup</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">        <span class="k">if</span> <span class="p">(</span><span class="nx">textBox</span><span class="p">.</span><span class="nx">value</span> <span class="o">===</span> <span class="s1">&#39;Error: You must type the error message into the text box to close this modal.&#39;</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">            <span class="nb">document</span><span class="p">.</span><span class="nx">body</span><span class="p">.</span><span class="nx">removeChild</span><span class="p">(</span><span class="nx">modal</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl"><span class="p">}</span>
</span></span></code></pre></div>]]></content:encoded>
    </item>
    
    <item>
      <title>Running Jupyter Lab Behind NGINX: Authentication and Securing the Proxy (Part 2)</title>
      <link>https://alex-jacobs.com/posts/jupyterlab2/</link>
      <pubDate>Wed, 15 Jun 2022 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/jupyterlab2/</guid>
      <description>Part 2 of the Jupyter Lab &#43; NGINX reverse proxy setup: configuring Jupyter authentication behind NGINX, handling token-based auth bypass, and locking down the proxy on EC2.</description>
      <content:encoded><![CDATA[<h4 id="if-you-havent-read-part-1postsjupyterlab1-you-may-want-to-start-there">If you haven&rsquo;t read <a href="/posts/jupyterlab1/">part 1</a>, you may want to start there.</h4>
<p>In the <a href="/posts/jupyterlab1/">last post</a>, we left off with a working reverse proxy, but we couldn&rsquo;t access Jupyter lab due to
its auth enforcement. Because of how we&rsquo;re setting this up, we will be handling
authentication upstream of Jupyter Lab, and we don&rsquo;t want to rely on them for handling authentication. What we are going to do here is generally considered &ldquo;unsafe.&rdquo;<br>
Again, if you&rsquo;re looking to do this for your team, check out <a href="https://jupyter.org/hub">Jupyter Hub</a>&ndash;it probably makes more sense
for your use case.</p>
<p>To disable token auth, we will update our Jupyter Lab config.</p>
<p>There is an extensive config file for Jupyter Lab. In a production environment, I recommend using it (you can generate a sample file
by running <code>jupyter notebook --generate-config</code>). But, for this toy example, we will pass
our config as cmd line args. To disable token auth and to allow same-origin requests, we&rsquo;re going to update our Jupyter Lab
Dockerfile Entrypoint to include these arguments</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="s2">&#34;--ServerApp.token=&#34;</span>, <span class="s2">&#34;--ServerApp.password=&#34;</span>, <span class="s2">&#34;--ServerApp.allow_origin&#34;</span>, <span class="s2">&#34;*&#34;</span>
</span></span></code></pre></div><p>Our Dockerfile should now look like</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="s"> ubuntu:20.04</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive
</span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3-pip <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3-dev <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3 -m pip install jupyterlab<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> useradd -ms /bin/bash jupyter<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">EXPOSE</span><span class="s"> 8888</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;jupyter&#34;</span><span class="p">,</span> <span class="s2">&#34;lab&#34;</span><span class="p">,</span> <span class="s2">&#34;--ip=0.0.0.0&#34;</span><span class="p">,</span> <span class="s2">&#34;--port&#34;</span><span class="p">,</span> <span class="s2">&#34;8888&#34;</span><span class="p">,</span> <span class="s2">&#34;--allow-root&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.token=&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.password=&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.allow_origin&#34;</span><span class="p">,</span> <span class="s2">&#34;*&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><p>And if we rebuild and start our docker compose again</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker compose build <span class="o">&amp;&amp;</span> docker compose up
</span></span></code></pre></div><p>We now get through to Juypter!
<img loading="lazy" src="/posts/jupyterlab2/img5.png" type="" alt="jupyter lab"  /></p>
<p>But if we try and open the Python kernel, we&rsquo;ll notice it&rsquo;s having trouble connecting.
<img loading="lazy" src="/posts/jupyterlab2/img3.png" type="" alt="jupyter lab_cant_connect"  /></p>
<p>Opening our browser dev tools shows that there is an issue with how our proxy is handling WebSockets
<img loading="lazy" src="/posts/jupyterlab2/img4.png" type="" alt="jupyter lab_websockets"  /></p>
<p>We&rsquo;ll have to update our Nginx config to address this.<br>
We will add these lines to set headers properly for WebSockets to our / location in the server block.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">...</span>
</span></span><span class="line"><span class="cl">  <span class="s">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="mi">8000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">localhost</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_set_header</span> <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_hide_header</span> <span class="s">&#34;X-Frame-Options&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_pass</span> <span class="s">http://upstream_jupyter</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        
</span></span><span class="line"><span class="cl">        <span class="c1"># websocket support
</span></span></span><span class="line"><span class="cl"><span class="c1"></span>        <span class="kn">proxy_http_version</span> <span class="mi">1</span><span class="s">.1</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_set_header</span> <span class="s">Upgrade</span> <span class="s">&#34;websocket&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_set_header</span> <span class="s">Connection</span> <span class="s">&#34;Upgrade&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_read_timeout</span> <span class="mi">86400</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">...</span>
</span></span></code></pre></div><p>And now, if we restart our containers using the updated config, we&rsquo;ll see our kernel connects!</p>
<p><img loading="lazy" src="/posts/jupyterlab2/img6.png" type="" alt="jupyter lab_websockets"  /></p>
<p>If you&rsquo;re wondering how we will handle security when we&rsquo;re basically giving whoever is using this a terminal
into our cloud, the answer is using AWS to isolate the instance via IAM roles/ policy. We aren&rsquo;t going to get too much into
that in this post, but it is a valid concern. There isn&rsquo;t much we can do to prevent a privilege escalation/container escape
from a sophisticated user, but we can at least not give root access.</p>
<p>We&rsquo;re going to update our Jupyter Dockerfile to have a new user, &lsquo;jupyter&rsquo;, and we&rsquo;ll run Jupyter Lab as this user.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl">...<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> useradd -ms /bin/bash jupyter<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">USER</span><span class="s"> jupyter</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span>...<span class="err">
</span></span></span></code></pre></div><p>We&rsquo;re also going to update our ENTRYPOINT, so the Jupyter Lab root directory is set to the Jupyter user&rsquo;s home directory</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="s2">&#34;--ServerApp.root_dir&#34;</span>, <span class="s2">&#34;/home/jupyter&#34;</span>, <span class="s2">&#34;--ServerApp.notebook_dir&#34;</span>, <span class="s2">&#34;/home/jupyter&#34;</span>
</span></span></code></pre></div><p>Our Dockerfile should now look like</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="s"> ubuntu:20.04</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive
</span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3-pip <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3-dev <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3 -m pip install jupyterlab<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="c"># add user and switch to them</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> useradd -ms /bin/bash jupyter<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">USER</span><span class="s"> jupyter</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">EXPOSE</span><span class="s"> 8888</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;jupyter&#34;</span><span class="p">,</span> <span class="s2">&#34;lab&#34;</span><span class="p">,</span> <span class="s2">&#34;--ip=0.0.0.0&#34;</span><span class="p">,</span> <span class="s2">&#34;--port&#34;</span><span class="p">,</span> <span class="s2">&#34;8888&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.token=&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.password=&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.allow_origin&#34;</span><span class="p">,</span> <span class="s2">&#34;*&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.root_dir&#34;</span><span class="p">,</span> <span class="s2">&#34;/home/jupyter&#34;</span><span class="p">,</span> <span class="s2">&#34;--ServerApp.notebook_dir&#34;</span><span class="p">,</span> <span class="s2">&#34;/home/jupyter&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><p>If we reload our site, we&rsquo;ll see that the working directory is now set to <code>/home/jupyter</code>, and if we try to
write to <code>/</code>, we&rsquo;ll get a permissions error. It&rsquo;s important to note that while this makes it a little more
difficult for a malicious user to take over this &lsquo;instance&rsquo;, we will be giving them access to the internet,
the ability to download and install packages, execute code, etc. It would not be too difficult for someone with mal
intent to get around this. Changing the user and working directory does more to help an innocent user from accidentally breaking
something.</p>
<p>Great! Now we have disabled token authentication, added a system user (who is now running Jupyter), and changed our notebook
directory to our user&rsquo;s directory! In the next post, we&rsquo;ll set up a task definition and deploy to ECS.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Running Jupyter Lab Behind NGINX: Reverse Proxy Setup with Docker (Part 1)</title>
      <link>https://alex-jacobs.com/posts/jupyterlab1/</link>
      <pubDate>Sun, 08 May 2022 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/jupyterlab1/</guid>
      <description>Jupyter Lab and NGINX aren&amp;#39;t alternatives — NGINX sits in front of Jupyter as a reverse proxy. Part 1 covers containerizing Jupyter Lab and wiring it to NGINX with a sidecar pattern on EC2.</description>
      <content:encoded><![CDATA[<h1 id="background">Background</h1>
<p>Jupyter Lab is an open source web-based IDE for notebooks with Python and R support, geared towards the data science crowd.
It&rsquo;s a powerful, mature application with a potentially complex configuration. Our requirement was to deliver Jupyter Lab to users
so that each user would have their own isolated &ldquo;instance .&rdquo; There is an off-the-shelf solution for this called Jupyter Hub that probably makes
the most sense for your organization. This example will be a proof of concept on how you could roll your solution.</p>
<h2 id="heading"></h2>
<h2 id="step-1--jupyter-lab-docker">Step 1&ndash;Jupyter Lab Docker</h2>
<h5 id="if-you-arent-familiar-with-docker-were-going-to-be-using-it-a-lot-here-so-check-out-some-guides">If you aren&rsquo;t familiar with Docker, we&rsquo;re going to be using it a lot here, so check out some guides</h5>
<p>Our first step will be getting Jupyter Lab up and running in a container.
There are many Docker images available on (Docker hub)[https://hub.docker.com/] for Jupyter Lab, but since we&rsquo;re rolling
everything ourselves, we might as well make our own image. It also gives us more control over our code&ndash;it&rsquo;s also a pretty simple Dockerfile.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="s"> ubuntu:20.04</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive
</span></span><span class="line"><span class="cl"><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3-pip <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>      python3-dev <span class="se">\
</span></span></span><span class="line"><span class="cl"><span class="se"></span>    <span class="o">&amp;&amp;</span> python3 -m pip install jupyterlab<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">EXPOSE</span><span class="s"> 8888</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;jupyter&#34;</span><span class="p">,</span> <span class="s2">&#34;lab&#34;</span><span class="p">,</span> <span class="s2">&#34;--ip=0.0.0.0&#34;</span><span class="p">,</span> <span class="s2">&#34;--port&#34;</span><span class="p">,</span> <span class="s2">&#34;8888&#34;</span><span class="p">,</span> <span class="s2">&#34;--allow-root&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><p>There are probably some good arguments for why you should use alpine or something else as the base image here, but I&rsquo;m
a sucker for ubuntu. Since this isn&rsquo;t a Docker tutorial, I&rsquo;m not going to go into great detail here about what each
line in this Dockerfile does, but assume that it installs Jupyter Lab and configures it to run at port 8888.
We&rsquo;ll expand on the Jupyter Lab config (and make some changes) later, but for now, this works fine.</p>
<p>We&rsquo;re going to use Docker compose to run this. Our compose file looks like</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jupyter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">dockerfile</span><span class="p">:</span><span class="w"> </span><span class="l">Dockerfile</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jupyter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jupyter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">8888</span><span class="p">:</span><span class="m">8888</span><span class="w">
</span></span></span></code></pre></div><p>We can run this with <code>docker compose up</code></p>
<p>This will start our Jupyter Lab container and make it available at <a href="http://127.0.0.1:8888/lab/">http://127.0.0.1:8888/lab/</a></p>
<p><img loading="lazy" src="/posts/jupyterlab1/img1.png" type="" alt="jupyter lab"  /></p>
<h3 id="awesome-were-part-of-the-way-there">Awesome! We&rsquo;re part of the way there!</h3>
<p>Next, we need to put together an Nginx docker file.</p>
<p>While we could use the official Nginx image, in keeping with the theme, we&rsquo;re going to create our own Nginx image (and it&rsquo;s also
<em>really</em> simple)</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="s"> ubuntu:20.04</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get -y install nginx<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">COPY</span> nginx.conf /etc/nginx/nginx.conf<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">EXPOSE</span><span class="s"> 8000</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;/usr/sbin/nginx&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><p>Pretty straightforward. Our config file is also pretty simple. We&rsquo;re going to use port 8000, and we&rsquo;re going to simply
forward all requests directly to Jupyter lab.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-nginx" data-lang="nginx"><span class="line"><span class="cl"><span class="k">daemon</span> <span class="no">off</span><span class="p">;</span>
</span></span><span class="line"><span class="cl"><span class="k">error_log</span> <span class="s">/dev/stdout</span> <span class="s">info</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">events</span> <span class="p">{}</span>
</span></span><span class="line"><span class="cl"><span class="k">http</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kn">access_log</span> <span class="s">/dev/stdout</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kn">upstream</span> <span class="s">upstream_jupyter</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server</span> <span class="n">jupyter</span><span class="p">:</span><span class="mi">8888</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">keepalive</span> <span class="mi">32</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="kn">server</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="kn">listen</span>       <span class="mi">8000</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="kn">server_name</span>  <span class="s">localhost</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="kn">location</span> <span class="s">/</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_set_header</span> <span class="s">Host</span> <span class="nv">$host</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_set_header</span> <span class="s">X-Real-IP</span> <span class="nv">$remote_addr</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_hide_header</span> <span class="s">&#34;X-Frame-Options&#34;</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">        <span class="kn">proxy_pass</span> <span class="s">http://upstream_jupyter</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></div><p>The final piece that will tie these together is our docker-compose file. Our docker-compose is pretty simple as well.
By using Docker compose, the networking between the containers is handled for us, and we can point the <code>jupyter</code> as the service
name in our nginx.conf</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">version</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w"></span><span class="nt">services</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">jupyter</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">dockerfile</span><span class="p">:</span><span class="w"> </span><span class="l">Dockerfile</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jupyter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">jupyter</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">8888</span><span class="p">:</span><span class="m">8888</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">nginx</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">build</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="l">.</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">dockerfile</span><span class="p">:</span><span class="w"> </span><span class="l">nginx.Dockerfile</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">image</span><span class="p">:</span><span class="w"> </span><span class="l">jupyter-nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">container_name</span><span class="p">:</span><span class="w"> </span><span class="l">nginx</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">ports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="m">8000</span><span class="p">:</span><span class="m">8000</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">volumes</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="l">nginx.conf:/etc/nginx/nginx.conf</span><span class="w">
</span></span></span></code></pre></div><p>Now, let&rsquo;s head to <a href="http://127.0.0.1:8000">http://127.0.0.1:8000</a>, and&hellip;</p>
<p><img loading="lazy" src="/posts/jupyterlab1/img2.png" type="" alt="nginx jupyter lab"  />
Awesome! We&rsquo;re being proxied to Jupyter Lab. But, we see a page requiring token auth. This is because Jupyter is currently
configured to enforce this. In <a href="/posts/jupyterlab2/">Part 2</a>, we&rsquo;ll deal with this and some other things regarding permissions, creating a user, and making a
task definition for deploying this configuration to ECS.</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>Splitting SRA into FASTQ with SRAToolkit, Python, and Docker</title>
      <link>https://alex-jacobs.com/posts/fastqsplit/</link>
      <pubDate>Sun, 03 Apr 2022 00:00:00 +0000</pubDate>
      
      <guid>https://alex-jacobs.com/posts/fastqsplit/</guid>
      <description>A simple example using Python and Docker to split an SRA file into Fastq</description>
      <content:encoded><![CDATA[<h1 id="background">Background</h1>
<p>SRA (Sequence Read Archive) is a file format used by NCBI, EBI, etc., for storing genomic read data. It works with
multiple file types (BAM, HDF5, FASTQ). In our case, we&rsquo;re going to be focusing on FASTQ. The first step of many pipelines
is converting SRA into FASTQ, which will be our focus in this post.</p>
<p>If you&rsquo;re working as an individual or a scientist, you probably want to
go ahead and use SRA Toolkit to download your files. For our purposes here, though,
we&rsquo;re going to assume you already have your files downloaded (and are probably
using an implement outside of SRA Toolkit for file i/o)</p>
<p>SRA Toolkit is pretty frustrating to use. It is not designed for programmatic use or as part of larger systems
(and the developers seem hostile to the idea that someone would even try and do this 😲). It wants you to
do an interactive configuration on every install <a href="https://github.com/ncbi/sra-tools/issues/77">https://github.com/ncbi/sra-tools/issues/77</a>
We get around this with a dumb hack to make it think we&rsquo;ve gone through this process and configured it. That&rsquo;s what&rsquo;s happening in lines 25-26.
(it&rsquo;s kind of messy to create this config file like this, it would probably be better to make this as a file and copy it in,
but for our purposes, I want to contain everything in a single file with no external dependencies)</p>
<h2 id="dockerfile">Dockerfile</h2>
<p>We will be using Docker and Python for this, so our first step is to create a Dockerfile with the tools we need
installed. Here&rsquo;s our Dockerfile&ndash;I&rsquo;ve added comments to explain what I&rsquo;m doing, but if you don&rsquo;t know <em>anything</em> about Docker,
this isn&rsquo;t a day one tutorial, so check out one of those first.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c"># Were using ubuntu 20.04 as our base image</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="err"></span><span class="k">FROM</span><span class="s"> ubuntu:20.04</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="err"></span><span class="c"># Set DEBIAN_FRONTEND to non-interactive to avoid interactive configuration</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">DEBIAN_FRONTEND</span><span class="o">=</span>noninteractive
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c"># Set SRA toolkit version</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">SRATOOLKIT_VERSION</span><span class="o">=</span><span class="s2">&#34;3.0.0&#34;</span><span class="err">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">USER</span><span class="o">=</span>alex
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="k">ENV</span> <span class="nv">DATA</span><span class="o">=</span>/data<span class="err">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="err"></span><span class="c"># change our working directory</span><span class="err">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="s"> /opt</span><span class="err">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="err"></span>    <span class="c1"># udpate package list and install wget, python</span><span class="err">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="err"></span><span class="k">RUN</span> apt-get update <span class="o">&amp;&amp;</span> apt-get install -y <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>    wget <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>    python3 <span class="o">&amp;&amp;</span> ln -sf python3 /usr/bin/python <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>    <span class="c1"># download and decompress our version of SRA Toolkit</span><span class="err">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="err"></span>    wget https://ftp-trace.ncbi.nlm.nih.gov/sra/sdk/<span class="si">${</span><span class="nv">SRATOOLKIT_VERSION</span><span class="si">}</span>/sratoolkit.<span class="si">${</span><span class="nv">SRATOOLKIT_VERSION</span><span class="si">}</span>-ubuntu64.tar.gz <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>    tar xvf /opt/sratoolkit.<span class="si">${</span><span class="nv">SRATOOLKIT_VERSION</span><span class="si">}</span>-ubuntu64.tar.gz<span class="err">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="err"></span><span class="c"># add SRA toolkit binaries to our path</span><span class="err">
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="err"></span><span class="k">ENV</span> <span class="nv">PATH</span><span class="o">=</span>/opt/sratoolkit.3.0.0-ubuntu64/bin:<span class="si">${</span><span class="nv">PATH</span><span class="si">}</span><span class="err">
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="err"></span>    <span class="c1"># create our usser</span><span class="err">
</span></span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="err"></span><span class="k">RUN</span> useradd -ms /bin/bash <span class="si">${</span><span class="nv">USER</span><span class="si">}</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="se"></span>    <span class="c1"># This creates a file tricking SRA Toolkit into thinking we&#39;ve gone through the manual configuration</span><span class="err">
</span></span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="err"></span>    mkdir /home/<span class="si">${</span><span class="nv">USER</span><span class="si">}</span>/.ncbi/ <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="se"></span>    <span class="nb">echo</span> <span class="s1">&#39;/LIBS/GUID = &#34;mock-uid&#34;\nconfig/default = &#34;true&#34;&#39;</span> &gt; /home/<span class="si">${</span><span class="nv">USER</span><span class="si">}</span>/.ncbi/user-settings.mkfg <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">27</span><span class="cl"><span class="se"></span>    <span class="c1"># create a data directory to work out of and give ownership to our user</span><span class="err">
</span></span></span><span class="line"><span class="ln">28</span><span class="cl"><span class="err"></span>    mkdir <span class="si">${</span><span class="nv">DATA</span><span class="si">}</span> <span class="o">&amp;&amp;</span> chown <span class="si">${</span><span class="nv">USER</span><span class="si">}</span> <span class="si">${</span><span class="nv">DATA</span><span class="si">}</span><span class="err">
</span></span></span><span class="line"><span class="ln">29</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">30</span><span class="cl"><span class="err"></span><span class="c"># copy in our python script</span><span class="err">
</span></span></span><span class="line"><span class="ln">31</span><span class="cl"><span class="err"></span><span class="k">COPY</span> dump_fastq.py /usr/bin/<span class="err">
</span></span></span><span class="line"><span class="ln">32</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">33</span><span class="cl"><span class="err"></span><span class="c"># set our user</span><span class="err">
</span></span></span><span class="line"><span class="ln">34</span><span class="cl"><span class="err"></span><span class="k">USER</span><span class="s"> ${USER}</span><span class="err">
</span></span></span><span class="line"><span class="ln">35</span><span class="cl"><span class="err"></span><span class="k">WORKDIR</span><span class="s"> ${DATA}</span><span class="err">
</span></span></span><span class="line"><span class="ln">36</span><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="ln">37</span><span class="cl"><span class="err"></span><span class="k">ENTRYPOINT</span> <span class="p">[</span><span class="s2">&#34;python&#34;</span><span class="p">,</span> <span class="s2">&#34;/usr/bin/dump_fastq.py&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></div><h2 id="python">Python</h2>
<p>Our python script for this is pretty simple. We&rsquo;re assuming that our SRA file has been
downloaded. We&rsquo;re going to be running SRA Toolkit using the python subprocess module.</p>
<p>First, we need to check that our SRA is valid. We use the <code>vdb-validate</code> tool to do this. If we get a good
return code, we will test if it&rsquo;s paired-ended data. I&rsquo;m not going to go into a ton of detail
about paired vs. single-ended data but suffice it to say that it&rsquo;s more effective to use paired-end data. You can read more here <a href="https://www.illumina.com/science/technology/next-generation-sequencing/plan-experiments/paired-end-vs-single-read.html">https://www.illumina.com/science/technology/next-generation-sequencing/plan-experiments/paired-end-vs-single-read.html</a></p>
<p>In most cases, data from modern experiments is paired, but it&rsquo;s essential to know. In this case, it may seem like it&rsquo;s not helpful,
but in a production env we would be passing these files on to another step in a pipeline, and we need to be
able to tell if we will have a single read file or multiple to configure the next step properly.</p>
<p>To determine this, we stand on the shoulders of those who&rsquo;ve come before us, and use a version of a function described here
<a href="https://www.biostars.org/p/139422/">https://www.biostars.org/p/139422/</a>.</p>
<p>Once we determine the data type, we&rsquo;ll pass our SRA to <code>fastq-dump</code> to split it. We&rsquo;re going to
tell it to give us gzipped output files. If the result is successful, we&rsquo;re simply going to return the paths of the files
(which, in this case, we&rsquo;re just going to log to stdout rather than upload)</p>
<p>That&rsquo;s basically it. We&rsquo;ve also added some simple error handling. I&rsquo;ve commented the file below to explain better what
we&rsquo;re doing.</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">sys</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="kn">import</span> <span class="nn">logging</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="kn">import</span> <span class="nn">argparse</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="kn">from</span> <span class="nn">subprocess</span> <span class="kn">import</span> <span class="n">run</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="n">logging</span><span class="o">.</span><span class="n">basicConfig</span><span class="p">(</span><span class="n">level</span><span class="o">=</span><span class="n">logging</span><span class="o">.</span><span class="n">INFO</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="n">log</span> <span class="o">=</span> <span class="n">logging</span><span class="o">.</span><span class="n">getLogger</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="k">def</span> <span class="nf">error</span><span class="p">(</span><span class="n">message</span><span class="o">=</span><span class="s1">&#39;Unexpected Error&#39;</span><span class="p">,</span> <span class="n">error</span><span class="o">=</span><span class="kc">None</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="n">log</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">message</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">    <span class="n">log</span><span class="o">.</span><span class="n">error</span><span class="p">(</span><span class="n">error</span><span class="p">)</span> <span class="k">if</span> <span class="n">error</span> <span class="k">else</span> <span class="kc">None</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">    <span class="n">sys</span><span class="o">.</span><span class="n">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="k">def</span> <span class="nf">is_paired_sra</span><span class="p">(</span><span class="n">sra</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">run</span><span class="p">([</span><span class="s1">&#39;fastq-dump&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">                      <span class="s1">&#39;-X&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">                      <span class="s1">&#39;1&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">                      <span class="s1">&#39;-Z&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">                      <span class="s1">&#39;--split-spot&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl">                      <span class="n">sra</span><span class="p">],</span> <span class="n">capture_output</span><span class="o">=</span><span class="kc">True</span><span class="p">,</span> <span class="n">text</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">        <span class="c1"># check for good return</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">        <span class="k">if</span> <span class="n">result</span><span class="o">.</span><span class="n">returncode</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl">            <span class="c1"># get number of lines in stdout</span>
</span></span><span class="line"><span class="ln">27</span><span class="cl">            <span class="n">num_lines</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">result</span><span class="o">.</span><span class="n">stdout</span><span class="o">.</span><span class="n">splitlines</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">28</span><span class="cl">            <span class="c1"># 4 lines indicates single end fastq</span>
</span></span><span class="line"><span class="ln">29</span><span class="cl">            <span class="k">if</span> <span class="n">num_lines</span> <span class="o">==</span> <span class="mi">4</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">30</span><span class="cl">                <span class="k">return</span> <span class="kc">False</span>
</span></span><span class="line"><span class="ln">31</span><span class="cl">            <span class="c1"># 8 lines indicates paired end</span>
</span></span><span class="line"><span class="ln">32</span><span class="cl">            <span class="k">elif</span> <span class="n">num_lines</span> <span class="o">==</span> <span class="mi">8</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">33</span><span class="cl">                <span class="k">return</span> <span class="kc">True</span>
</span></span><span class="line"><span class="ln">34</span><span class="cl">            <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">35</span><span class="cl">                <span class="c1"># There are cases where an index may be included, and 12 lines would be output</span>
</span></span><span class="line"><span class="ln">36</span><span class="cl">                <span class="c1"># for our purposes here, we are going to treat this as an error</span>
</span></span><span class="line"><span class="ln">37</span><span class="cl">                <span class="n">error</span><span class="p">(</span><span class="s1">&#39;Unable to determine if SRA is paired ended&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">38</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">39</span><span class="cl">            <span class="n">error</span><span class="p">(</span><span class="s1">&#39;Unable to determine if SRA is paired ended&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">40</span><span class="cl">
</span></span><span class="line"><span class="ln">41</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">42</span><span class="cl">        <span class="n">error</span><span class="p">(</span><span class="n">error</span><span class="o">=</span><span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">43</span><span class="cl">
</span></span><span class="line"><span class="ln">44</span><span class="cl">
</span></span><span class="line"><span class="ln">45</span><span class="cl"><span class="k">def</span> <span class="nf">validate</span><span class="p">(</span><span class="n">sra</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">46</span><span class="cl">    <span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">47</span><span class="cl">        <span class="c1"># validate sra, return result, report error</span>
</span></span><span class="line"><span class="ln">48</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">run</span><span class="p">([</span><span class="s1">&#39;vdb-validate&#39;</span><span class="p">,</span> <span class="n">sra</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">49</span><span class="cl">        <span class="k">return</span> <span class="n">result</span><span class="o">.</span><span class="n">returncode</span> <span class="o">==</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">50</span><span class="cl">    <span class="k">except</span> <span class="ne">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">51</span><span class="cl">        <span class="n">error</span><span class="p">(</span><span class="n">error</span><span class="o">=</span><span class="n">e</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">52</span><span class="cl">
</span></span><span class="line"><span class="ln">53</span><span class="cl">
</span></span><span class="line"><span class="ln">54</span><span class="cl"><span class="k">def</span> <span class="nf">split_sra</span><span class="p">(</span><span class="n">sra</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">55</span><span class="cl">    <span class="c1"># check if sra is paired</span>
</span></span><span class="line"><span class="ln">56</span><span class="cl">    <span class="n">paired</span> <span class="o">=</span> <span class="n">is_paired_sra</span><span class="p">(</span><span class="n">sra</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">57</span><span class="cl">    <span class="c1"># dump sra into fastq</span>
</span></span><span class="line"><span class="ln">58</span><span class="cl">    <span class="n">result</span> <span class="o">=</span> <span class="n">run</span><span class="p">([</span><span class="s1">&#39;fastq-dump&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">59</span><span class="cl">                  <span class="s1">&#39;--split-files&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">60</span><span class="cl">                  <span class="s1">&#39;--gzip&#39;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">61</span><span class="cl">                  <span class="s1">&#39;--outdir&#39;</span><span class="p">,</span> <span class="n">results_dir</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">62</span><span class="cl">                  <span class="n">sra</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">63</span><span class="cl">
</span></span><span class="line"><span class="ln">64</span><span class="cl">    <span class="k">if</span> <span class="n">result</span><span class="o">.</span><span class="n">returncode</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">65</span><span class="cl">        <span class="c1"># in prod, we would likely upload the files here. For our purposes we are just going to report their local paths</span>
</span></span><span class="line"><span class="ln">66</span><span class="cl">        <span class="k">if</span> <span class="n">paired</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">67</span><span class="cl">            <span class="c1"># if data is paired return both read 1 and read 2</span>
</span></span><span class="line"><span class="ln">68</span><span class="cl">            <span class="k">return</span> <span class="sa">f</span><span class="s1">&#39;</span><span class="si">{</span><span class="n">sra</span><span class="o">.</span><span class="n">strip</span><span class="p">(</span><span class="s2">&#34;.sra&#34;</span><span class="p">)</span><span class="si">}</span><span class="s1">_1.fastq.gz&#39;</span><span class="p">,</span> <span class="sa">f</span><span class="s1">&#39;</span><span class="si">{</span><span class="n">sra</span><span class="o">.</span><span class="n">strip</span><span class="p">(</span><span class="s2">&#34;.sra&#34;</span><span class="p">)</span><span class="si">}</span><span class="s1">_2.fastq.gz&#39;</span>
</span></span><span class="line"><span class="ln">69</span><span class="cl">        <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">70</span><span class="cl">            <span class="c1"># otherwise, only read 1</span>
</span></span><span class="line"><span class="ln">71</span><span class="cl">            <span class="k">return</span> <span class="sa">f</span><span class="s1">&#39;</span><span class="si">{</span><span class="n">results_dir</span><span class="si">}</span><span class="s1">/</span><span class="si">{</span><span class="n">sra</span><span class="o">.</span><span class="n">strip</span><span class="p">(</span><span class="s2">&#34;.sra&#34;</span><span class="p">)</span><span class="si">}</span><span class="s1">_1.fastq.gz&#39;</span>
</span></span><span class="line"><span class="ln">72</span><span class="cl">
</span></span><span class="line"><span class="ln">73</span><span class="cl">
</span></span><span class="line"><span class="ln">74</span><span class="cl"><span class="k">def</span> <span class="nf">main</span><span class="p">(</span><span class="n">sra</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">75</span><span class="cl">    <span class="c1"># check for valid sra file</span>
</span></span><span class="line"><span class="ln">76</span><span class="cl">    <span class="k">if</span> <span class="n">validate</span><span class="p">(</span><span class="n">sra</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">77</span><span class="cl">        <span class="c1"># dump sra, report results</span>
</span></span><span class="line"><span class="ln">78</span><span class="cl">        <span class="n">result</span> <span class="o">=</span> <span class="n">split_sra</span><span class="p">(</span><span class="n">sra</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">79</span><span class="cl">        <span class="n">log</span><span class="o">.</span><span class="n">info</span><span class="p">(</span><span class="n">result</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">80</span><span class="cl">    <span class="k">else</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">81</span><span class="cl">        <span class="n">error</span><span class="p">(</span><span class="s1">&#39;SRA is is not valid&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">82</span><span class="cl">
</span></span><span class="line"><span class="ln">83</span><span class="cl">
</span></span><span class="line"><span class="ln">84</span><span class="cl"><span class="k">if</span> <span class="vm">__name__</span> <span class="o">==</span> <span class="s2">&#34;__main__&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">85</span><span class="cl">    <span class="n">parser</span> <span class="o">=</span> <span class="n">argparse</span><span class="o">.</span><span class="n">ArgumentParser</span><span class="p">(</span><span class="n">description</span><span class="o">=</span><span class="s1">&#39;Split SRA&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">86</span><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;--sra&#39;</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">&#39;Path to SRA&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">87</span><span class="cl">    <span class="n">parser</span><span class="o">.</span><span class="n">add_argument</span><span class="p">(</span><span class="s1">&#39;-data_dir&#39;</span><span class="p">,</span> <span class="n">default</span><span class="o">=</span><span class="s1">&#39;/data&#39;</span><span class="p">,</span> <span class="n">required</span><span class="o">=</span><span class="kc">False</span><span class="p">,</span> <span class="nb">type</span><span class="o">=</span><span class="nb">str</span><span class="p">,</span> <span class="n">help</span><span class="o">=</span><span class="s1">&#39;Data directory&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">88</span><span class="cl">    <span class="n">args</span> <span class="o">=</span> <span class="n">parser</span><span class="o">.</span><span class="n">parse_args</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">89</span><span class="cl">    <span class="n">results_dir</span> <span class="o">=</span> <span class="n">args</span><span class="o">.</span><span class="n">data_dir</span>
</span></span><span class="line"><span class="ln">90</span><span class="cl">    <span class="n">main</span><span class="p">(</span><span class="sa">f</span><span class="s1">&#39;</span><span class="si">{</span><span class="n">results_dir</span><span class="si">}</span><span class="s1">/</span><span class="si">{</span><span class="n">args</span><span class="o">.</span><span class="n">sra</span><span class="si">}</span><span class="s1">&#39;</span><span class="p">)</span>
</span></span></code></pre></div><h2 id="testing-out-our-code">Testing out our code</h2>
<p>Next, we&rsquo;re going to need some data. SRA files are often pretty large (sometimes hundreds of gigabytes).<br>
Typically, S. cerevisiae RNAseq datasets are pretty small but also fully functional, so we&rsquo;re going to
be using one of those <a href="https://trace.ncbi.nlm.nih.gov/Traces/?run=SRR21712309">https://trace.ncbi.nlm.nih.gov/Traces/?run=SRR21712309</a>
(To find this, I went to <a href="https://ncbi.nlm.nih.gov/sra">https://ncbi.nlm.nih.gov/sra</a>, entered S. cerevisiae in the search bar, and selected the first one :smiling:)</p>
<p>Now that we have data, we can run this container using Docker. We will
bind a directory on our host machine (<code>./data</code>) to our <code>/data</code> directory in our container. This folder is where our input data will live, and the Fastq files our script generates will be written.</p>
<p>To run, we&rsquo;ll simply run,</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">docker run -v<span class="nv">$PWD</span>/data:/data fastq-dump --sra SRR21712309
</span></span></code></pre></div><p>and we&rsquo;ll see output logged to stdout</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">2022-10-12T01:21:16 vdb-validate.3.0.0 info: Database <span class="s1">&#39;SRR21712309&#39;</span> metadata: md5 ok
</span></span><span class="line"><span class="cl">2022-10-12T01:21:16 vdb-validate.3.0.0 info: Table <span class="s1">&#39;SEQUENCE&#39;</span> metadata: md5 ok
</span></span><span class="line"><span class="cl">2022-10-12T01:21:16 vdb-validate.3.0.0 info: Column <span class="s1">&#39;ALTREAD&#39;</span>: checksums ok
</span></span><span class="line"><span class="cl">2022-10-12T01:21:17 vdb-validate.3.0.0 info: Column <span class="s1">&#39;QUALITY&#39;</span>: checksums ok
</span></span><span class="line"><span class="cl">2022-10-12T01:21:20 vdb-validate.3.0.0 info: Column <span class="s1">&#39;READ&#39;</span>: checksums ok
</span></span><span class="line"><span class="cl">2022-10-12T01:21:21 vdb-validate.3.0.0 info: Database <span class="s1">&#39;/data/SRR21712309&#39;</span> contains only unaligned reads
</span></span><span class="line"><span class="cl">2022-10-12T01:21:21 vdb-validate.3.0.0 info: Database <span class="s1">&#39;SRR21712309&#39;</span> is consistent
</span></span><span class="line"><span class="cl">Read <span class="m">8631759</span> spots <span class="k">for</span> /data/SRR21712309
</span></span><span class="line"><span class="cl">Written <span class="m">8631759</span> spots <span class="k">for</span> /data/SRR21712309
</span></span><span class="line"><span class="cl">INFO:root:<span class="o">(</span><span class="s1">&#39;/data/SRR21712309_1.fastq.gz&#39;</span>, <span class="s1">&#39;/data/SRR21712309_2.fastq.gz&#39;</span><span class="o">)</span>
</span></span></code></pre></div><p>We&rsquo;ll also see these files appear in our <code>./data</code> directory. The path to these will match
our file paths logged at the end</p>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="o">(</span>venv<span class="o">)</span> alexjacobs@Alexs-MacBook-Pro ~/r/b/fastq-dump <span class="o">(</span>master<span class="o">)</span>&gt; ls -lh data
</span></span><span class="line"><span class="cl">total <span class="m">1354280</span>
</span></span><span class="line"><span class="cl">-rw-r--r--@ <span class="m">1</span> alexjacobs  staff   213M Sep <span class="m">27</span> 07:30 SRR21712309
</span></span><span class="line"><span class="cl">-rw-r--r--  <span class="m">1</span> alexjacobs  staff   217M Oct <span class="m">11</span> 22:57 SRR21712309_1.fastq.gz
</span></span><span class="line"><span class="cl">-rw-r--r--  <span class="m">1</span> alexjacobs  staff   221M Oct <span class="m">11</span> 22:57 SRR21712309_2.fastq.gz
</span></span></code></pre></div><p>And that&rsquo;s it!  This is a lot of explanation for a simple toy example, but hopefully is helpful
to someone just getting started!</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
