<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Anthony Addae</title>
    <description>The latest articles on DEV Community by Anthony Addae (@maaddae).</description>
    <link>https://dev.to/maaddae</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3528371%2F02c07b04-058c-4c77-bb0f-4960fb5e2441.jpeg</url>
      <title>DEV Community: Anthony Addae</title>
      <link>https://dev.to/maaddae</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/maaddae"/>
    <language>en</language>
    <item>
      <title>Building Durable, AI-Powered Workflows in Django</title>
      <dc:creator>Anthony Addae</dc:creator>
      <pubDate>Fri, 11 Sep 2026 10:31:13 +0000</pubDate>
      <link>https://dev.to/maaddae/building-durable-ai-powered-workflows-in-django-2dga</link>
      <guid>https://dev.to/maaddae/building-durable-ai-powered-workflows-in-django-2dga</guid>
      <description>&lt;p&gt;This tutorial walks through a complete local development path for this project:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Start the Django application and its dependencies with Docker Compose.&lt;/li&gt;
&lt;li&gt;Install Git pre-commit hooks so quality checks run before each commit.&lt;/li&gt;
&lt;li&gt;Create a product through the workflow API.&lt;/li&gt;
&lt;li&gt;Follow the product as Temporal calculates a price, waits for approval, updates the price, and publishes the product.&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;The intended reader is comfortable opening a terminal but new to Docker Compose and Temporal. You do not need to install Python or PostgreSQL directly on your computer: the local Compose setup runs those application dependencies in containers.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  What We Are Building
&lt;/h2&gt;

&lt;p&gt;The project is a Django application backed by PostgreSQL. Product onboarding is asynchronous, so the HTTP request does not perform every step itself. Instead, Django creates the product and starts a &lt;a href="https://temporal.io/" rel="noopener noreferrer"&gt;Temporal&lt;/a&gt; workflow.&lt;/p&gt;

&lt;p&gt;The local stack contains:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;th&gt;Local address&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;django&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Django development server and migrations&lt;/td&gt;
&lt;td&gt;&lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;postgres&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Application database&lt;/td&gt;
&lt;td&gt;Internal Compose network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temporal&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Temporal server&lt;/td&gt;
&lt;td&gt;&lt;code&gt;localhost:7233&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temporal-worker&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Executes workflow code and activities&lt;/td&gt;
&lt;td&gt;Internal Compose network&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;temporal-ui&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Inspect workflow executions&lt;/td&gt;
&lt;td&gt;&lt;a href="http://localhost:8088" rel="noopener noreferrer"&gt;http://localhost:8088&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;mailpit&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Captures development email&lt;/td&gt;
&lt;td&gt;&lt;a href="http://localhost:8025" rel="noopener noreferrer"&gt;http://localhost:8025&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The service definitions live in &lt;code&gt;docker-compose.local.yml&lt;/code&gt;. Compose gives services stable names on its internal network, which is why Django connects to &lt;code&gt;postgres&lt;/code&gt; and the worker connects to &lt;code&gt;temporal&lt;/code&gt; instead of &lt;code&gt;localhost&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwpdo80l6kgka4w9ylns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwpdo80l6kgka4w9ylns.png" alt=" " width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;p&gt;Install these tools first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://docs.docker.com/desktop/" rel="noopener noreferrer"&gt;Docker Desktop&lt;/a&gt;, including Docker Compose&lt;/li&gt;
&lt;li&gt;&lt;a href="https://git-scm.com/downloads" rel="noopener noreferrer"&gt;Git&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/casey/just" rel="noopener noreferrer"&gt;just&lt;/a&gt;, a command runner used by this repository&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Check that they are available:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker &lt;span class="nt"&gt;--version&lt;/span&gt;
docker compose version
git &lt;span class="nt"&gt;--version&lt;/span&gt;
just &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clone the repository and enter it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/klhenams/verbose-spork.git
&lt;span class="nb"&gt;cd &lt;/span&gt;app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 1: Check Local Configuration
&lt;/h2&gt;

&lt;p&gt;The Compose file loads environment values from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;.envs/.local/.django
.envs/.local/.postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important local settings are:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;POSTGRES_HOST=postgres&lt;/code&gt;, because PostgreSQL is another Compose service.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TEMPORAL_HOST=temporal:7233&lt;/code&gt;, supplied to the Django and worker containers.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HUGGINGFACEHUB_API_TOKEN&lt;/code&gt;, which is optional for this tutorial.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not commit real credentials or API tokens. If your checkout does not contain local environment files, create them from your development configuration. The Django file can contain an empty Hugging Face token while learning the workflow; the application has a deterministic cost-based fallback.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Start Docker Compose
&lt;/h2&gt;

&lt;p&gt;The repository's &lt;code&gt;justfile&lt;/code&gt; selects &lt;code&gt;docker-compose.local.yml&lt;/code&gt; automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just up
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under the hood, this runs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose &lt;span class="nt"&gt;-f&lt;/span&gt; docker-compose.local.yml up &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="nt"&gt;--remove-orphans&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first start may take a while because Docker builds the Django and PostgreSQL images and downloads Temporal images. The Django container runs &lt;code&gt;python manage.py migrate&lt;/code&gt; before starting the development server, so the database schema is prepared automatically.&lt;/p&gt;

&lt;p&gt;Check the running containers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open these pages in a browser:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Django: &lt;a href="http://localhost:8000" rel="noopener noreferrer"&gt;http://localhost:8000&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;API documentation: &lt;a href="http://localhost:8000/api/docs/" rel="noopener noreferrer"&gt;http://localhost:8000/api/docs/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Temporal UI: &lt;a href="http://localhost:8088" rel="noopener noreferrer"&gt;http://localhost:8088&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Mailpit: &lt;a href="http://localhost:8025" rel="noopener noreferrer"&gt;http://localhost:8025&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the application is still starting, inspect the logs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just logs django
just logs temporal-worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker should eventually log that it is listening on the &lt;code&gt;product-tasks&lt;/code&gt; task queue. A Temporal worker must be running for workflow activities to execute; starting only Django is not enough.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Install Git Pre-Commit Hooks
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit&lt;/a&gt; runs configured checks against staged files before Git creates a commit. This repository declares its hooks in &lt;code&gt;.pre-commit-config.yaml&lt;/code&gt; and installs the development dependency from &lt;code&gt;pyproject.toml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Install the hook inside the Django container so the host machine does not need the project's Python version:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; django pre-commit &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run all hooks once against the whole repository:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; django pre-commit run &lt;span class="nt"&gt;--all-files&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration checks common file problems, applies Django upgrades, runs Ruff linting and formatting, formats &lt;code&gt;pyproject.toml&lt;/code&gt;, and validates Django templates with djLint. Some hooks can modify files. Review those changes, stage them, and run the command again until it passes.&lt;/p&gt;

&lt;p&gt;To run the checks manually on only staged files:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; django pre-commit run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration deliberately excludes documentation and migrations from the hook selection. That does not mean documentation cannot be reviewed; it means the project avoids applying code-formatting hooks to those paths.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: Run the Tests
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;justfile&lt;/code&gt; provides a containerized pytest command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just pytest
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For the pricing activity tests only:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just pytest app/products/tests/test_workflow_activities.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pricing activity has two paths:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;With &lt;code&gt;HUGGINGFACEHUB_API_TOKEN&lt;/code&gt;, it asks the configured Hugging Face model for a structured &lt;code&gt;PriceSuggestion&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Without a token, it uses a fallback of &lt;code&gt;cost * 1.35&lt;/code&gt;, rounded to two decimal places.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That fallback makes the local workflow usable without an external model account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Understand the Product Creation Endpoint
&lt;/h2&gt;

&lt;p&gt;The workflow-specific endpoint is registered as &lt;code&gt;workflows&lt;/code&gt; in the product API router. Because the project includes the API router below &lt;code&gt;/api/&lt;/code&gt;, create a product with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST http://localhost:8000/api/workflows/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The endpoint requires authentication. Use the API documentation at &lt;a href="http://localhost:8000/api/docs/" rel="noopener noreferrer"&gt;http://localhost:8000/api/docs/&lt;/a&gt; or obtain a DRF token from the project's authentication endpoint before sending the request.&lt;/p&gt;

&lt;p&gt;The request body should include the product fields below. &lt;code&gt;category&lt;/code&gt; is an existing category ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Portable Monitor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"A lightweight second screen for a laptop."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MONITOR-PORTABLE-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"100.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stock_quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"low_stock_threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The initial &lt;code&gt;price&lt;/code&gt; is allowed to be a placeholder. The workflow will calculate a suggestion and later write the selected price. The create view saves the product with &lt;code&gt;pending_pricing&lt;/code&gt; and starts a workflow with the ID:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;product-onboarding-{product_id}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, product &lt;code&gt;42&lt;/code&gt; runs as &lt;code&gt;product-onboarding-42&lt;/code&gt; on Temporal task queue &lt;code&gt;product-tasks&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Follow the Workflow Sequentially
&lt;/h2&gt;

&lt;p&gt;The workflow is implemented in &lt;code&gt;app/products/workflows/workflows.py&lt;/code&gt;. Its activities are in &lt;code&gt;app/products/workflows/activities.py&lt;/code&gt;.&lt;br&gt;
&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4qsgya6wjj20qf2q7870.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F4qsgya6wjj20qf2q7870.png" alt=" " width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  1. Calculate a suggested price
&lt;/h3&gt;

&lt;p&gt;The worker executes &lt;code&gt;calculate_suggested_market_price&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;With no Hugging Face token and a cost of &lt;code&gt;100.00&lt;/code&gt;, the fallback suggestion is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;100.00 * 1.35 = 135.00 GHS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The activity returns a structured result containing the suggested price, currency, confidence score, reasoning, and source. If the external model fails, the activity logs the exception and uses the fallback instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inspect the suggestion
&lt;/h3&gt;

&lt;p&gt;The API can query the running workflow without changing its state:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;GET http://localhost:8000/api/workflows/{product_id}/pricing-suggestion/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"suggestion"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"suggested_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;135.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"recommended_margin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"35.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"GHS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"confidence_score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reasoning"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Fallback pricing based on product cost."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cost_fallback"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow includes a short ten-second pause after pricing so the execution is easy to observe in the Temporal UI. That delay is for demonstration and is not required by the business process.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Approve or override the price
&lt;/h3&gt;

&lt;p&gt;The workflow waits for a signal named &lt;code&gt;approve_suggested_price&lt;/code&gt;. Send the suggested price as-is or choose another value:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST http://localhost:8000/api/workflows/{product_id}/approve-price/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Request body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"129.99"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API translates this request into a Temporal signal. The workflow records the selected price and continues. This is a useful distinction:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A &lt;strong&gt;query&lt;/strong&gt; reads workflow state.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;signal&lt;/strong&gt; sends an event that changes workflow state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If no approval arrives before the configured timeout, the workflow automatically selects the suggested price. The default timeout is 24 hours.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Update the product price
&lt;/h3&gt;

&lt;p&gt;After approval or timeout, &lt;code&gt;update_product_price&lt;/code&gt; stores the final price in PostgreSQL. The product remains controlled by the workflow rather than being published immediately by the HTTP request.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Publish with retries
&lt;/h3&gt;

&lt;p&gt;The workflow calls &lt;code&gt;publish_product&lt;/code&gt; with a Temporal retry policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Maximum of three attempts&lt;/li&gt;
&lt;li&gt;Two-second initial interval&lt;/li&gt;
&lt;li&gt;Exponential backoff with a coefficient of &lt;code&gt;2.0&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When publishing succeeds, the activity changes the product status to &lt;code&gt;active&lt;/code&gt;, and the workflow returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PUBLISHED"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"final_price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;129.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"was_auto_approved"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: See the Compensation Path
&lt;/h2&gt;

&lt;p&gt;To exercise the failure path, include the test-only field below when creating a product:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Failure Demo"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Used to demonstrate workflow compensation."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sku"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FAILURE-DEMO-001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"10.00"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stock_quantity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"low_stock_threshold"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_should_fail_publish"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"test_timeout_seconds"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The publish activity raises an error deliberately. Temporal retries it three times. When all attempts fail, the workflow executes &lt;code&gt;revert_product_status&lt;/code&gt;, which changes the database status to &lt;code&gt;failed_publish&lt;/code&gt;, and returns a &lt;code&gt;FAILED_PUBLISH&lt;/code&gt; result.&lt;/p&gt;

&lt;p&gt;This is a small &lt;a href="https://microservices.io/patterns/data/saga.html" rel="noopener noreferrer"&gt;Saga pattern&lt;/a&gt;: after earlier work has changed state, a compensation activity restores a meaningful failure state when a later operation cannot complete.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;test_*&lt;/code&gt; fields are passed through by the current API view to make demonstrations and tests predictable. Treat them as development/testing controls, not as production business inputs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Observe Everything in Temporal UI
&lt;/h2&gt;

&lt;p&gt;Open &lt;a href="http://localhost:8088" rel="noopener noreferrer"&gt;http://localhost:8088&lt;/a&gt; and find the workflow named &lt;code&gt;product-onboarding-{product_id}&lt;/code&gt;. You can inspect:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The workflow status&lt;/li&gt;
&lt;li&gt;The activity attempts and retry history&lt;/li&gt;
&lt;li&gt;The waiting period for human approval&lt;/li&gt;
&lt;li&gt;The final return value&lt;/li&gt;
&lt;li&gt;The failure and compensation sequence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is often easier to understand than reading logs because Temporal shows the workflow's event history as a durable sequence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stopping and Resetting the Environment
&lt;/h2&gt;

&lt;p&gt;Stop the containers but keep the PostgreSQL volume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just down
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Stop the containers and delete local volumes, including the database data:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just prune
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use &lt;code&gt;just prune&lt;/code&gt; carefully. The next &lt;code&gt;just up&lt;/code&gt; starts with an empty database and will need to run migrations and recreate any sample data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Troubleshooting
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The workflow never advances
&lt;/h3&gt;

&lt;p&gt;Check the worker:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;just logs temporal-worker
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The worker must connect to &lt;code&gt;temporal:7233&lt;/code&gt; and register the &lt;code&gt;product-tasks&lt;/code&gt; queue. If only the Django container is running, the workflow can start but its activities cannot be picked up.&lt;/p&gt;

&lt;h3&gt;
  
  
  Django cannot connect to PostgreSQL
&lt;/h3&gt;

&lt;p&gt;Inside Compose, use &lt;code&gt;postgres&lt;/code&gt; as the database host, not &lt;code&gt;localhost&lt;/code&gt;. Check that the database health check has passed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;docker compose ps postgres
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  The pricing suggestion is not from Hugging Face
&lt;/h3&gt;

&lt;p&gt;That is expected when &lt;code&gt;HUGGINGFACEHUB_API_TOKEN&lt;/code&gt; is empty or the model request fails. The cost-based fallback is intentionally part of the local implementation.&lt;/p&gt;

&lt;h3&gt;
  
  
  Pre-commit changes a file
&lt;/h3&gt;

&lt;p&gt;Review and stage the change, then run the hook again:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git diff
git add &lt;span class="nt"&gt;-A&lt;/span&gt;
docker compose run &lt;span class="nt"&gt;--rm&lt;/span&gt; django pre-commit run
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What to Read Next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/" rel="noopener noreferrer"&gt;Docker Compose documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/compose/how-tos/networking/" rel="noopener noreferrer"&gt;Docker Compose networking&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pre-commit.com/" rel="noopener noreferrer"&gt;pre-commit documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/develop/python" rel="noopener noreferrer"&gt;Temporal Python SDK documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.temporal.io/develop/python/message-passing" rel="noopener noreferrer"&gt;Temporal workflow signals and queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://www.django-rest-framework.org/api-guide/viewsets/" rel="noopener noreferrer"&gt;Django REST framework ViewSets&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.djangoproject.com/en/stable/topics/migrations/" rel="noopener noreferrer"&gt;Django database migrations&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The central idea is simple: Django accepts the request and records intent, while Temporal coordinates the long-running work, human decision, retries, and compensation. Docker Compose makes that entire system reproducible on a developer laptop.&lt;/p&gt;

</description>
      <category>docker</category>
      <category>django</category>
      <category>python</category>
      <category>temporal</category>
    </item>
    <item>
      <title>Open Source Is People: A Python &amp; Django Year</title>
      <dc:creator>Anthony Addae</dc:creator>
      <pubDate>Wed, 31 Dec 2025 10:31:03 +0000</pubDate>
      <link>https://dev.to/maaddae/open-source-is-people-a-python-django-year-com</link>
      <guid>https://dev.to/maaddae/open-source-is-people-a-python-django-year-com</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;For years I stood just outside the circle, watching talks, mentoring quietly, and cheering from the edges. In 2025, I finally stepped into the light.&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is my first-ever blog post. Good people of Python Ghana community kept nudging, “Start a blog.” The last gentle push came from the Djangonaut Space program. So here it is: the beginning of a habit of showing up, reflecting, and documenting the journey. I also share photos on my photoblog, &lt;a href="https://www.instagram.com/maaddae/" rel="noopener noreferrer"&gt;@maaddae&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;My First Step: Django Girls Ho 2017&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;It started in &lt;strong&gt;2017&lt;/strong&gt;, in Ho. I showed up as a &lt;strong&gt;&lt;a href="https://djangogirls.org/" rel="noopener noreferrer"&gt;Django Girls&lt;/a&gt;&lt;/strong&gt; mentor for a workshop organized by &lt;strong&gt;Noah Alorwu&lt;/strong&gt;. One room, a handful of laptops, and the quiet buzz of first lines of code, something clicked. That day opened a door.&lt;/p&gt;

&lt;p&gt;It opened the door to a welcoming community of people who build, teach, and support each other. For years after that, I stayed involved mostly through mentorship. I taught when I could, encouraged beginners, and contributed quietly.&lt;/p&gt;

&lt;p&gt;But in 2025, something shifted. I felt ready to take a bolder step forward.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Starting the Year With Mentorship Once Again&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Just like in 2017, 2025 began with people.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Django Girls Koforidua 2025&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I joined as a mentor, helping participants build their first Django applications. The moment a beginner connects the dots, from views to templates to a working app, never gets old. That spark keeps me returning.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;Django Girls Ho 2025&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;Later, I coached at Django Girls Ho. It felt like coming home: nostalgic, grounding, and inspiring. Open source, at its best, is made of patience, kindness, and shared growth.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Building Community Through Speaking and Organizing&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Then the year nudged me forward, beyond mentorship, into roles that stretched me.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;PyCon Ghana 2025&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I served as a &lt;strong&gt;programme co-organizer&lt;/strong&gt; for &lt;strong&gt;&lt;a href="https://pyconghana.org/" rel="noopener noreferrer"&gt;PyCon Ghana&lt;/a&gt;&lt;/strong&gt;: reviewing talks, shaping the agenda, supporting speakers. I also spoke and joined a panel. Wearing both hats taught me how much care it takes to craft a meaningful conference.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;PyHo Conference 2025&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I returned to speak at &lt;strong&gt;&lt;a href="https://2025.pythonho.com/" rel="noopener noreferrer"&gt;PyHo&lt;/a&gt;&lt;/strong&gt;, one of Ghana’s most vibrant Python gatherings. The warmth and curiosity there always feels like home.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;strong&gt;PyCon Africa 2025&lt;/strong&gt;
&lt;/h3&gt;

&lt;p&gt;I was selected to speak at &lt;strong&gt;&lt;a href="https://africa.pycon.org/" rel="noopener noreferrer"&gt;PyCon Africa&lt;/a&gt;&lt;/strong&gt;, a milestone I celebrated. Then, an airport desk and a paperwork snag turned celebration into absence. It stung, but the community’s support turned the moment into fuel rather than a full stop.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Joining the Django Software Foundation&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;A quiet, meaningful milestone followed: I became an &lt;strong&gt;Individual Member of the &lt;a href="https://www.djangoproject.com/foundation/" rel="noopener noreferrer"&gt;Django Software Foundation&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;For someone who has loved Django for years, that recognition felt like a promise: to keep contributing, to engage deeply, to show up.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Djangonaut Space: A Transformative Chapter&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Another turning point: I was accepted into &lt;strong&gt;&lt;a href="https://djangonaut.space/" rel="noopener noreferrer"&gt;Djangonaut Space&lt;/a&gt;&lt;/strong&gt; and joined &lt;strong&gt;Team Neptune&lt;/strong&gt;. With mentors who’ve shaped Django, I submitted my &lt;strong&gt;first pull request&lt;/strong&gt; to the framework. It came back with review comments, welcomed notes on how to do better. I am committed to keep going beyond the program.&lt;/p&gt;

&lt;p&gt;It wasn’t just technical. It connected me to the heart of the framework I’ve relied on for years, and to the people who keep it alive.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;Why This Year Matters to Me&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Looking back, 2025 feels like a year of showing up: learning, teaching, sharing, and stepping further in.&lt;/p&gt;

&lt;p&gt;I started the year mostly mentoring. I end it as a mentor, speaker, organizer, contributor, and a community member actively shaping the spaces that shaped me.&lt;/p&gt;

&lt;p&gt;Writing this blog is part of that growth, and I hope to keep it up.&lt;/p&gt;




&lt;h2&gt;
  
  
  &lt;strong&gt;The Road Ahead&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;As I look forward, I hope to continue:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;contributing to Django core&lt;/li&gt;
&lt;li&gt;supporting Django Girls workshops&lt;/li&gt;
&lt;li&gt;speaking at Python and Django events&lt;/li&gt;
&lt;li&gt;mentoring upcoming developers&lt;/li&gt;
&lt;li&gt;strengthening local and global communities&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Open source has room for everyone. I am grateful that I found my place, and I hope this encourages someone out there to start their own journey too.&lt;/p&gt;

</description>
      <category>opensource</category>
      <category>python</category>
      <category>django</category>
      <category>yearinreview</category>
    </item>
  </channel>
</rss>
