{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Configuration Merge Pattern: Secure Config Management Made Simple\n", "\n", "## The Security Problem\n", "\n", "### 🚨 Never Commit Secrets to Version Control\n", "\n", "In production applications, you face a critical security challenge: **how to manage configuration without exposing sensitive data**.\n", "\n", "**The Dilemma:**\n", "- ✅ **Non-sensitive config** (hosts, ports, timeouts) → Safe to version control\n", "- ❌ **Sensitive config** (passwords, API keys, certificates) → Must NEVER be in version control\n", "\n", "**What Goes Wrong:**" ] }, { "cell_type": "code", "execution_count": 1, "metadata": { "ExecuteTime": { "end_time": "2023-05-31T00:14:16.269653Z", "start_time": "2023-05-31T00:14:16.265586Z" }, "collapsed": false, "jupyter": { "outputs_hidden": false }, "tags": [] }, "outputs": [], "source": [ "# ❌ DANGEROUS: All config in one file\n", "config = {\n", " \"database\": {\n", " \"host\": \"prod-db.company.com\", # Safe to share\n", " \"port\": 5432, # Safe to share \n", " \"username\": \"app_user\", # Safe to share\n", " \"password\": \"super_secret_123!\" # 🚨 LEAKED TO GIT!\n", " }\n", "}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Real-world consequences:**\n", "- 🔓 **Credentials exposed** in git history (permanent damage)\n", "- 🤖 **Bots scraping** GitHub for secrets (immediate exploitation) \n", "- 👥 **Team members** accidentally access production secrets\n", "- 🔄 **Copy-paste errors** spreading secrets across environments\n", "\n", "---\n", "\n", "## Why Existing Solutions Fall Short\n", "\n", "### The Problems with Manual Config Assembly\n", "\n", "Let's see what happens when developers try to solve this manually:" ] }, { "cell_type": "code", "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2023-05-31T00:14:16.319137Z", "start_time": "2023-05-31T00:14:16.271334Z" }, "collapsed": false, "jupyter": { "outputs_hidden": false }, "tags": [] }, "outputs": [], "source": [ "# Setup for demonstration\n", "import json\n", "from rich import print as rprint\n", "\n", "def jprint(data: dict):\n", " \"\"\"Pretty print JSON data\"\"\"\n", " rprint(json.dumps(data, indent=2))" ] }, { "cell_type": "code", "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2023-05-31T00:14:16.319302Z", "start_time": "2023-05-31T00:14:16.311381Z" }, "collapsed": false, "jupyter": { "outputs_hidden": false }, "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "❌ Broken result with dict.update():\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"database\": {\n",
       "    \"password\": \"secret123\"\n",
       "  },\n",
       "  \"cache\": {\n",
       "    \"password\": \"redis-secret\"\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"secret123\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"cache\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"redis-secret\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ❌ Attempt 1: Simple dict.update() \n", "base_config = {\n", " \"database\": {\"host\": \"prod-db.com\", \"port\": 5432, \"pool_size\": 20},\n", " \"cache\": {\"host\": \"redis.com\", \"ttl\": 3600}\n", "}\n", "\n", "secrets = {\n", " \"database\": {\"password\": \"secret123\"}, # Only has password\n", " \"cache\": {\"password\": \"redis-secret\"} # Only has password\n", "}\n", "\n", "# This OVERWRITES the entire nested dict!\n", "broken_config = base_config.copy()\n", "broken_config.update(secrets)\n", "\n", "print(\"❌ Broken result with dict.update():\")\n", "jprint(broken_config)" ] }, { "cell_type": "code", "execution_count": 4, "metadata": { "collapsed": false, "jupyter": { "outputs_hidden": false }, "tags": [] }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "⚠️ Manual merge - works but limited:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"database\": {\n",
       "    \"host\": \"prod-db.com\",\n",
       "    \"port\": 5432,\n",
       "    \"pool_size\": 20,\n",
       "    \"password\": \"secret123\"\n",
       "  },\n",
       "  \"cache\": {\n",
       "    \"host\": \"redis.com\",\n",
       "    \"ttl\": 3600,\n",
       "    \"password\": \"redis-secret\"\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-db.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"pool_size\"\u001b[0m: \u001b[1;36m20\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"secret123\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"cache\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"redis.com\"\u001b[0m,\n", " \u001b[32m\"ttl\"\u001b[0m: \u001b[1;36m3600\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"redis-secret\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ❌ Attempt 2: Manual nested merging\n", "def manual_merge(base, secrets):\n", " result = base.copy()\n", " for key, value in secrets.items():\n", " if key in result and isinstance(result[key], dict) and isinstance(value, dict):\n", " # Manual nested merge - but what about deeper nesting? Lists?\n", " result[key].update(value)\n", " else:\n", " result[key] = value\n", " return result\n", "\n", "manual_result = manual_merge(base_config, secrets)\n", "print(\"⚠️ Manual merge - works but limited:\")\n", "jprint(manual_result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Problems with manual approaches:**\n", "- 🔄 **Doesn't handle deep nesting** (3+ levels)\n", "- 📋 **Can't merge lists** (loses relationships)\n", "- 🐛 **No type validation** (silent data corruption)\n", "- 🚀 **Not reusable** (reimplemented everywhere)\n", "- ❌ **No error reporting** (fails silently)\n", "\n", "---\n", "\n", "## The Smart Merge Solution\n", "\n", "### Introducing `deep_merge`\n", "\n", "The `deep_merge` function provides **intelligent, structure-aware merging** that solves all the problems above:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Smart merge result:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"database\": {\n",
       "    \"host\": \"prod-db.com\",\n",
       "    \"port\": 5432,\n",
       "    \"pool_size\": 20,\n",
       "    \"password\": \"secret123\"\n",
       "  },\n",
       "  \"cache\": {\n",
       "    \"host\": \"redis.com\",\n",
       "    \"ttl\": 3600,\n",
       "    \"password\": \"redis-secret\"\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-db.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"pool_size\"\u001b[0m: \u001b[1;36m20\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"secret123\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"cache\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"redis.com\"\u001b[0m,\n", " \u001b[32m\"ttl\"\u001b[0m: \u001b[1;36m3600\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"redis-secret\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "from configcraft.api import deep_merge\n", "\n", "# ✅ The same data as before\n", "base_config = {\n", " \"database\": {\"host\": \"prod-db.com\", \"port\": 5432, \"pool_size\": 20},\n", " \"cache\": {\"host\": \"redis.com\", \"ttl\": 3600}\n", "}\n", "\n", "secrets = {\n", " \"database\": {\"password\": \"secret123\"},\n", " \"cache\": {\"password\": \"redis-secret\"}\n", "}\n", "\n", "# ✅ Smart merging\n", "smart_result = deep_merge(base_config, secrets)\n", "print(\"✅ Smart merge result:\")\n", "jprint(smart_result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Why This Works Better\n", "\n", "**🧠 Intelligence:** Understands data structure and preserves relationships \n", "**🛡️ Safety:** Immutable operations (never modifies originals) \n", "**🔍 Validation:** Type-checked merging with clear error messages \n", "**📈 Scalability:** Handles arbitrary nesting depth and complexity \n", "\n", "---\n", "\n", "## Core Merge Behaviors\n", "\n", "Understanding how `deep_merge` works will help you predict and control the merge behavior:\n", "\n", "### 1. Adding New Keys (No Conflicts)\n", "\n", "When keys exist in only one dictionary, they're simply added:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "New keys are simply added:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"dev\": {\n",
       "    \"host\": \"dev-server.com\"\n",
       "  },\n",
       "  \"prod\": {\n",
       "    \"host\": \"prod-server.com\"\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"dev\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-server.com\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"prod\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-server.com\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "config_data = {\n", " \"dev\": {\"host\": \"dev-server.com\"}\n", "}\n", "secrets_data = {\n", " \"prod\": {\"host\": \"prod-server.com\"} # Completely new environment\n", "}\n", "\n", "result = deep_merge(config_data, secrets_data)\n", "print(\"New keys are simply added:\")\n", "jprint(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Recursive Dictionary Merging\n", "\n", "When both inputs have the same key with dictionary values, they merge recursively:" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Recursive dictionary merging:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"database\": {\n",
       "    \"host\": \"db.company.com\",\n",
       "    \"port\": 5432,\n",
       "    \"connection\": {\n",
       "      \"timeout\": 30,\n",
       "      \"retry_attempts\": 3,\n",
       "      \"ssl_cert\": \"/path/to/cert.pem\"\n",
       "    },\n",
       "    \"password\": \"secret_password\",\n",
       "    \"username\": \"app_user\"\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"db.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"connection\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"timeout\"\u001b[0m: \u001b[1;36m30\u001b[0m,\n", " \u001b[32m\"retry_attempts\"\u001b[0m: \u001b[1;36m3\u001b[0m,\n", " \u001b[32m\"ssl_cert\"\u001b[0m: \u001b[32m\"/path/to/cert.pem\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"secret_password\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"app_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "config_data = {\n", " \"database\": {\n", " \"host\": \"db.company.com\",\n", " \"port\": 5432,\n", " \"connection\": {\n", " \"timeout\": 30,\n", " \"retry_attempts\": 3\n", " }\n", " }\n", "}\n", "\n", "secrets_data = {\n", " \"database\": {\n", " \"username\": \"app_user\",\n", " \"password\": \"secret_password\",\n", " \"connection\": {\n", " \"ssl_cert\": \"/path/to/cert.pem\"\n", " }\n", " }\n", "}\n", "\n", "result = deep_merge(config_data, secrets_data)\n", "print(\"Recursive dictionary merging:\")\n", "jprint(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Positional List Merging\n", "\n", "**Critical Behavior:** Lists are merged **by position** to maintain relationships:" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Positional list merging maintains relationships:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"users\": [\n",
       "    {\n",
       "      \"username\": \"alice\",\n",
       "      \"role\": \"admin\",\n",
       "      \"department\": \"engineering\",\n",
       "      \"password\": \"alice_secure_123\"\n",
       "    },\n",
       "    {\n",
       "      \"username\": \"bob\",\n",
       "      \"role\": \"user\",\n",
       "      \"department\": \"sales\",\n",
       "      \"password\": \"bob_password_456\"\n",
       "    },\n",
       "    {\n",
       "      \"username\": \"charlie\",\n",
       "      \"role\": \"moderator\",\n",
       "      \"department\": \"support\",\n",
       "      \"password\": \"charlie_secret_789\"\n",
       "    }\n",
       "  ]\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"users\"\u001b[0m: \u001b[1m[\u001b[0m\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"alice\"\u001b[0m,\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"admin\"\u001b[0m,\n", " \u001b[32m\"department\"\u001b[0m: \u001b[32m\"engineering\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"alice_secure_123\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"bob\"\u001b[0m,\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"user\"\u001b[0m,\n", " \u001b[32m\"department\"\u001b[0m: \u001b[32m\"sales\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"bob_password_456\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"charlie\"\u001b[0m,\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"moderator\"\u001b[0m,\n", " \u001b[32m\"department\"\u001b[0m: \u001b[32m\"support\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"charlie_secret_789\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m]\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# User configuration with roles\n", "user_config = {\n", " \"users\": [\n", " {\"username\": \"alice\", \"role\": \"admin\", \"department\": \"engineering\"},\n", " {\"username\": \"bob\", \"role\": \"user\", \"department\": \"sales\"},\n", " {\"username\": \"charlie\", \"role\": \"moderator\", \"department\": \"support\"}\n", " ]\n", "}\n", "\n", "# Corresponding passwords (same order!)\n", "password_config = {\n", " \"users\": [\n", " {\"password\": \"alice_secure_123\"}, # For alice\n", " {\"password\": \"bob_password_456\"}, # For bob \n", " {\"password\": \"charlie_secret_789\"} # For charlie\n", " ]\n", "}\n", "\n", "result = deep_merge(user_config, password_config)\n", "print(\"Positional list merging maintains relationships:\")\n", "jprint(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Why positional merging matters:**\n", "- 🎯 **Maintains relationships**: user[0] password matches user[0] username\n", "- 🔒 **Security critical**: Wrong password assignments = security breach\n", "- 📊 **Data integrity**: Preserves logical connections between data elements\n", "\n", "---\n", "\n", "## Step-by-Step Guide\n", "\n", "### Basic Configuration + Secrets Pattern\n", "\n", "The most common use case: separating configuration from secrets.\n", "\n", "#### Step 1: Create your base configuration" ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "📄 Base configuration (safe to commit):\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"app_name\": \"MyApplication\",\n",
       "  \"environments\": {\n",
       "    \"dev\": {\n",
       "      \"database\": {\n",
       "        \"host\": \"dev-db.company.com\",\n",
       "        \"port\": 5432,\n",
       "        \"name\": \"myapp_dev\",\n",
       "        \"pool_size\": 5\n",
       "      },\n",
       "      \"redis\": {\n",
       "        \"host\": \"dev-redis.company.com\",\n",
       "        \"port\": 6379,\n",
       "        \"db\": 0\n",
       "      },\n",
       "      \"features\": {\n",
       "        \"debug_mode\": true,\n",
       "        \"rate_limiting\": false\n",
       "      }\n",
       "    },\n",
       "    \"prod\": {\n",
       "      \"database\": {\n",
       "        \"host\": \"prod-db.company.com\",\n",
       "        \"port\": 5432,\n",
       "        \"name\": \"myapp_prod\",\n",
       "        \"pool_size\": 20\n",
       "      },\n",
       "      \"redis\": {\n",
       "        \"host\": \"prod-redis.company.com\",\n",
       "        \"port\": 6379,\n",
       "        \"db\": 1\n",
       "      },\n",
       "      \"features\": {\n",
       "        \"debug_mode\": false,\n",
       "        \"rate_limiting\": true\n",
       "      }\n",
       "    }\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"app_name\"\u001b[0m: \u001b[32m\"MyApplication\"\u001b[0m,\n", " \u001b[32m\"environments\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"dev\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-db.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"myapp_dev\"\u001b[0m,\n", " \u001b[32m\"pool_size\"\u001b[0m: \u001b[1;36m5\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"redis\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-redis.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m6379\u001b[0m,\n", " \u001b[32m\"db\"\u001b[0m: \u001b[1;36m0\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"features\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"debug_mode\"\u001b[0m: true,\n", " \u001b[32m\"rate_limiting\"\u001b[0m: false\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"prod\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-db.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"myapp_prod\"\u001b[0m,\n", " \u001b[32m\"pool_size\"\u001b[0m: \u001b[1;36m20\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"redis\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-redis.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m6379\u001b[0m,\n", " \u001b[32m\"db\"\u001b[0m: \u001b[1;36m1\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"features\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"debug_mode\"\u001b[0m: false,\n", " \u001b[32m\"rate_limiting\"\u001b[0m: true\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# config.json - Safe to commit to version control\n", "base_config = {\n", " \"app_name\": \"MyApplication\",\n", " \"environments\": {\n", " \"dev\": {\n", " \"database\": {\n", " \"host\": \"dev-db.company.com\", \n", " \"port\": 5432,\n", " \"name\": \"myapp_dev\",\n", " \"pool_size\": 5\n", " },\n", " \"redis\": {\n", " \"host\": \"dev-redis.company.com\",\n", " \"port\": 6379,\n", " \"db\": 0\n", " },\n", " \"features\": {\n", " \"debug_mode\": True,\n", " \"rate_limiting\": False\n", " }\n", " },\n", " \"prod\": {\n", " \"database\": {\n", " \"host\": \"prod-db.company.com\",\n", " \"port\": 5432, \n", " \"name\": \"myapp_prod\",\n", " \"pool_size\": 20\n", " },\n", " \"redis\": {\n", " \"host\": \"prod-redis.company.com\",\n", " \"port\": 6379,\n", " \"db\": 1\n", " },\n", " \"features\": {\n", " \"debug_mode\": False,\n", " \"rate_limiting\": True\n", " }\n", " }\n", " }\n", "}\n", "\n", "print(\"📄 Base configuration (safe to commit):\")\n", "jprint(base_config)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Step 2: Create your secrets configuration" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🔒 Secrets configuration (NEVER commit):\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"environments\": {\n",
       "    \"dev\": {\n",
       "      \"database\": {\n",
       "        \"username\": \"dev_user\",\n",
       "        \"password\": \"dev_secret_123\"\n",
       "      },\n",
       "      \"redis\": {\n",
       "        \"password\": \"dev_redis_pwd\"\n",
       "      },\n",
       "      \"api_keys\": {\n",
       "        \"stripe\": \"sk_test_dev_key_123\",\n",
       "        \"sendgrid\": \"SG.dev.api.key\"\n",
       "      }\n",
       "    },\n",
       "    \"prod\": {\n",
       "      \"database\": {\n",
       "        \"username\": \"prod_user\",\n",
       "        \"password\": \"super_secure_prod_password_456\"\n",
       "      },\n",
       "      \"redis\": {\n",
       "        \"password\": \"prod_redis_secure_789\"\n",
       "      },\n",
       "      \"api_keys\": {\n",
       "        \"stripe\": \"sk_live_prod_key_789\",\n",
       "        \"sendgrid\": \"SG.prod.live.key\"\n",
       "      }\n",
       "    }\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"environments\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"dev\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"dev_user\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_secret_123\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"redis\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_redis_pwd\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"api_keys\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"stripe\"\u001b[0m: \u001b[32m\"sk_test_dev_key_123\"\u001b[0m,\n", " \u001b[32m\"sendgrid\"\u001b[0m: \u001b[32m\"SG.dev.api.key\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"prod\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"prod_user\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"super_secure_prod_password_456\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"redis\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"prod_redis_secure_789\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"api_keys\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"stripe\"\u001b[0m: \u001b[32m\"sk_live_prod_key_789\"\u001b[0m,\n", " \u001b[32m\"sendgrid\"\u001b[0m: \u001b[32m\"SG.prod.live.key\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# secrets.json - NEVER commit to version control\n", "# Load from environment variables, secret management system, etc.\n", "secrets_config = {\n", " \"environments\": {\n", " \"dev\": {\n", " \"database\": {\n", " \"username\": \"dev_user\",\n", " \"password\": \"dev_secret_123\"\n", " },\n", " \"redis\": {\n", " \"password\": \"dev_redis_pwd\"\n", " },\n", " \"api_keys\": {\n", " \"stripe\": \"sk_test_dev_key_123\",\n", " \"sendgrid\": \"SG.dev.api.key\"\n", " }\n", " },\n", " \"prod\": {\n", " \"database\": {\n", " \"username\": \"prod_user\", \n", " \"password\": \"super_secure_prod_password_456\"\n", " },\n", " \"redis\": {\n", " \"password\": \"prod_redis_secure_789\"\n", " },\n", " \"api_keys\": {\n", " \"stripe\": \"sk_live_prod_key_789\",\n", " \"sendgrid\": \"SG.prod.live.key\"\n", " }\n", " }\n", " }\n", "}\n", "\n", "print(\"🔒 Secrets configuration (NEVER commit):\")\n", "jprint(secrets_config)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Step 3: Merge them safely" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Final merged configuration:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"app_name\": \"MyApplication\",\n",
       "  \"environments\": {\n",
       "    \"dev\": {\n",
       "      \"database\": {\n",
       "        \"host\": \"dev-db.company.com\",\n",
       "        \"port\": 5432,\n",
       "        \"name\": \"myapp_dev\",\n",
       "        \"pool_size\": 5,\n",
       "        \"password\": \"dev_secret_123\",\n",
       "        \"username\": \"dev_user\"\n",
       "      },\n",
       "      \"redis\": {\n",
       "        \"host\": \"dev-redis.company.com\",\n",
       "        \"port\": 6379,\n",
       "        \"db\": 0,\n",
       "        \"password\": \"dev_redis_pwd\"\n",
       "      },\n",
       "      \"features\": {\n",
       "        \"debug_mode\": true,\n",
       "        \"rate_limiting\": false\n",
       "      },\n",
       "      \"api_keys\": {\n",
       "        \"stripe\": \"sk_test_dev_key_123\",\n",
       "        \"sendgrid\": \"SG.dev.api.key\"\n",
       "      }\n",
       "    },\n",
       "    \"prod\": {\n",
       "      \"database\": {\n",
       "        \"host\": \"prod-db.company.com\",\n",
       "        \"port\": 5432,\n",
       "        \"name\": \"myapp_prod\",\n",
       "        \"pool_size\": 20,\n",
       "        \"password\": \"super_secure_prod_password_456\",\n",
       "        \"username\": \"prod_user\"\n",
       "      },\n",
       "      \"redis\": {\n",
       "        \"host\": \"prod-redis.company.com\",\n",
       "        \"port\": 6379,\n",
       "        \"db\": 1,\n",
       "        \"password\": \"prod_redis_secure_789\"\n",
       "      },\n",
       "      \"features\": {\n",
       "        \"debug_mode\": false,\n",
       "        \"rate_limiting\": true\n",
       "      },\n",
       "      \"api_keys\": {\n",
       "        \"stripe\": \"sk_live_prod_key_789\",\n",
       "        \"sendgrid\": \"SG.prod.live.key\"\n",
       "      }\n",
       "    }\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"app_name\"\u001b[0m: \u001b[32m\"MyApplication\"\u001b[0m,\n", " \u001b[32m\"environments\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"dev\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-db.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"myapp_dev\"\u001b[0m,\n", " \u001b[32m\"pool_size\"\u001b[0m: \u001b[1;36m5\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_secret_123\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"dev_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"redis\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-redis.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m6379\u001b[0m,\n", " \u001b[32m\"db\"\u001b[0m: \u001b[1;36m0\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_redis_pwd\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"features\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"debug_mode\"\u001b[0m: true,\n", " \u001b[32m\"rate_limiting\"\u001b[0m: false\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"api_keys\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"stripe\"\u001b[0m: \u001b[32m\"sk_test_dev_key_123\"\u001b[0m,\n", " \u001b[32m\"sendgrid\"\u001b[0m: \u001b[32m\"SG.dev.api.key\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"prod\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-db.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"myapp_prod\"\u001b[0m,\n", " \u001b[32m\"pool_size\"\u001b[0m: \u001b[1;36m20\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"super_secure_prod_password_456\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"prod_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"redis\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-redis.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m6379\u001b[0m,\n", " \u001b[32m\"db\"\u001b[0m: \u001b[1;36m1\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"prod_redis_secure_789\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"features\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"debug_mode\"\u001b[0m: false,\n", " \u001b[32m\"rate_limiting\"\u001b[0m: true\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"api_keys\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"stripe\"\u001b[0m: \u001b[32m\"sk_live_prod_key_789\"\u001b[0m,\n", " \u001b[32m\"sendgrid\"\u001b[0m: \u001b[32m\"SG.prod.live.key\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Merge configurations \n", "final_config = deep_merge(base_config, secrets_config)\n", "\n", "print(\"✅ Final merged configuration:\")\n", "jprint(final_config)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Step 4: Use environment-specific config" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🎯 Configuration for dev environment:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"database\": {\n",
       "    \"host\": \"dev-db.company.com\",\n",
       "    \"port\": 5432,\n",
       "    \"name\": \"myapp_dev\",\n",
       "    \"pool_size\": 5,\n",
       "    \"password\": \"dev_secret_123\",\n",
       "    \"username\": \"dev_user\"\n",
       "  },\n",
       "  \"redis\": {\n",
       "    \"host\": \"dev-redis.company.com\",\n",
       "    \"port\": 6379,\n",
       "    \"db\": 0,\n",
       "    \"password\": \"dev_redis_pwd\"\n",
       "  },\n",
       "  \"features\": {\n",
       "    \"debug_mode\": true,\n",
       "    \"rate_limiting\": false\n",
       "  },\n",
       "  \"api_keys\": {\n",
       "    \"stripe\": \"sk_test_dev_key_123\",\n",
       "    \"sendgrid\": \"SG.dev.api.key\"\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-db.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"myapp_dev\"\u001b[0m,\n", " \u001b[32m\"pool_size\"\u001b[0m: \u001b[1;36m5\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_secret_123\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"dev_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"redis\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-redis.company.com\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m6379\u001b[0m,\n", " \u001b[32m\"db\"\u001b[0m: \u001b[1;36m0\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_redis_pwd\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"features\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"debug_mode\"\u001b[0m: true,\n", " \u001b[32m\"rate_limiting\"\u001b[0m: false\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"api_keys\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"stripe\"\u001b[0m: \u001b[32m\"sk_test_dev_key_123\"\u001b[0m,\n", " \u001b[32m\"sendgrid\"\u001b[0m: \u001b[32m\"SG.dev.api.key\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Extract environment-specific configuration\n", "env = \"dev\" # or \"prod\" in production\n", "app_config = final_config[\"environments\"][env]\n", "\n", "print(f\"🎯 Configuration for {env} environment:\")\n", "jprint(app_config)\n", "\n", "# Now you can safely use this config in your application\n", "# database_url = f\"postgresql://{app_config['database']['username']}:{app_config['database']['password']}@{app_config['database']['host']}:{app_config['database']['port']}/{app_config['database']['name']}\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Advanced Use Cases\n", "\n", "### Complex List Merging with Multiple Services\n", "\n", "Real-world applications often have lists of services, databases, or API endpoints:" ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🔧 Merged service configuration:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"microservices\": [\n",
       "    {\n",
       "      \"name\": \"user-service\",\n",
       "      \"port\": 8001,\n",
       "      \"replicas\": 3,\n",
       "      \"health_check\": \"/health\",\n",
       "      \"db_password\": \"user_db_secret\",\n",
       "      \"api_key\": \"user_service_key_123\"\n",
       "    },\n",
       "    {\n",
       "      \"name\": \"order-service\",\n",
       "      \"port\": 8002,\n",
       "      \"replicas\": 2,\n",
       "      \"health_check\": \"/status\",\n",
       "      \"db_password\": \"order_db_secret\",\n",
       "      \"api_key\": \"order_service_key_456\"\n",
       "    },\n",
       "    {\n",
       "      \"name\": \"payment-service\",\n",
       "      \"port\": 8003,\n",
       "      \"replicas\": 5,\n",
       "      \"health_check\": \"/ping\",\n",
       "      \"db_password\": \"payment_db_secret\",\n",
       "      \"api_key\": \"payment_service_key_789\"\n",
       "    }\n",
       "  ]\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"microservices\"\u001b[0m: \u001b[1m[\u001b[0m\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"user-service\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m8001\u001b[0m,\n", " \u001b[32m\"replicas\"\u001b[0m: \u001b[1;36m3\u001b[0m,\n", " \u001b[32m\"health_check\"\u001b[0m: \u001b[32m\"/health\"\u001b[0m,\n", " \u001b[32m\"db_password\"\u001b[0m: \u001b[32m\"user_db_secret\"\u001b[0m,\n", " \u001b[32m\"api_key\"\u001b[0m: \u001b[32m\"user_service_key_123\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"order-service\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m8002\u001b[0m,\n", " \u001b[32m\"replicas\"\u001b[0m: \u001b[1;36m2\u001b[0m,\n", " \u001b[32m\"health_check\"\u001b[0m: \u001b[32m\"/status\"\u001b[0m,\n", " \u001b[32m\"db_password\"\u001b[0m: \u001b[32m\"order_db_secret\"\u001b[0m,\n", " \u001b[32m\"api_key\"\u001b[0m: \u001b[32m\"order_service_key_456\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"payment-service\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m8003\u001b[0m,\n", " \u001b[32m\"replicas\"\u001b[0m: \u001b[1;36m5\u001b[0m,\n", " \u001b[32m\"health_check\"\u001b[0m: \u001b[32m\"/ping\"\u001b[0m,\n", " \u001b[32m\"db_password\"\u001b[0m: \u001b[32m\"payment_db_secret\"\u001b[0m,\n", " \u001b[32m\"api_key\"\u001b[0m: \u001b[32m\"payment_service_key_789\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m]\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Base service configuration\n", "service_config = {\n", " \"microservices\": [\n", " {\n", " \"name\": \"user-service\",\n", " \"port\": 8001,\n", " \"replicas\": 3,\n", " \"health_check\": \"/health\"\n", " },\n", " {\n", " \"name\": \"order-service\", \n", " \"port\": 8002,\n", " \"replicas\": 2,\n", " \"health_check\": \"/status\"\n", " },\n", " {\n", " \"name\": \"payment-service\",\n", " \"port\": 8003,\n", " \"replicas\": 5,\n", " \"health_check\": \"/ping\"\n", " }\n", " ]\n", "}\n", "\n", "# Service secrets (API keys, database passwords, etc.)\n", "service_secrets = {\n", " \"microservices\": [\n", " {\n", " \"api_key\": \"user_service_key_123\",\n", " \"db_password\": \"user_db_secret\"\n", " },\n", " {\n", " \"api_key\": \"order_service_key_456\", \n", " \"db_password\": \"order_db_secret\"\n", " },\n", " {\n", " \"api_key\": \"payment_service_key_789\",\n", " \"db_password\": \"payment_db_secret\"\n", " }\n", " ]\n", "}\n", "\n", "merged_services = deep_merge(service_config, service_secrets)\n", "print(\"🔧 Merged service configuration:\")\n", "jprint(merged_services)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Multi-Environment Database Configuration" ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "🗄️ Complete database configuration:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"environments\": {\n",
       "    \"dev\": {\n",
       "      \"databases\": [\n",
       "        {\n",
       "          \"role\": \"primary\",\n",
       "          \"host\": \"dev-db-1.internal\",\n",
       "          \"port\": 5432,\n",
       "          \"password\": \"dev_primary_secret\",\n",
       "          \"username\": \"dev_primary_user\"\n",
       "        },\n",
       "        {\n",
       "          \"role\": \"replica\",\n",
       "          \"host\": \"dev-db-2.internal\",\n",
       "          \"port\": 5432,\n",
       "          \"password\": \"dev_replica_secret\",\n",
       "          \"username\": \"dev_replica_user\"\n",
       "        }\n",
       "      ]\n",
       "    },\n",
       "    \"prod\": {\n",
       "      \"databases\": [\n",
       "        {\n",
       "          \"role\": \"primary\",\n",
       "          \"host\": \"prod-db-1.internal\",\n",
       "          \"port\": 5432,\n",
       "          \"password\": \"prod_primary_secret\",\n",
       "          \"username\": \"prod_primary_user\"\n",
       "        },\n",
       "        {\n",
       "          \"role\": \"replica\",\n",
       "          \"host\": \"prod-db-2.internal\",\n",
       "          \"port\": 5432,\n",
       "          \"password\": \"prod_replica_secret_1\",\n",
       "          \"username\": \"prod_replica_user\"\n",
       "        },\n",
       "        {\n",
       "          \"role\": \"replica\",\n",
       "          \"host\": \"prod-db-3.internal\",\n",
       "          \"port\": 5432,\n",
       "          \"password\": \"prod_replica_secret_2\",\n",
       "          \"username\": \"prod_replica_user\"\n",
       "        }\n",
       "      ]\n",
       "    }\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"environments\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"dev\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"databases\"\u001b[0m: \u001b[1m[\u001b[0m\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"primary\"\u001b[0m,\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-db-1.internal\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_primary_secret\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"dev_primary_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"replica\"\u001b[0m,\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"dev-db-2.internal\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"dev_replica_secret\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"dev_replica_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m]\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[32m\"prod\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"databases\"\u001b[0m: \u001b[1m[\u001b[0m\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"primary\"\u001b[0m,\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-db-1.internal\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"prod_primary_secret\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"prod_primary_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"replica\"\u001b[0m,\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-db-2.internal\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"prod_replica_secret_1\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"prod_replica_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"role\"\u001b[0m: \u001b[32m\"replica\"\u001b[0m,\n", " \u001b[32m\"host\"\u001b[0m: \u001b[32m\"prod-db-3.internal\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m5432\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"prod_replica_secret_2\"\u001b[0m,\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"prod_replica_user\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m]\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Database topology configuration\n", "db_topology = {\n", " \"environments\": {\n", " \"dev\": {\n", " \"databases\": [\n", " {\"role\": \"primary\", \"host\": \"dev-db-1.internal\", \"port\": 5432},\n", " {\"role\": \"replica\", \"host\": \"dev-db-2.internal\", \"port\": 5432}\n", " ]\n", " },\n", " \"prod\": {\n", " \"databases\": [\n", " {\"role\": \"primary\", \"host\": \"prod-db-1.internal\", \"port\": 5432},\n", " {\"role\": \"replica\", \"host\": \"prod-db-2.internal\", \"port\": 5432},\n", " {\"role\": \"replica\", \"host\": \"prod-db-3.internal\", \"port\": 5432}\n", " ]\n", " }\n", " }\n", "}\n", "\n", "# Database credentials (different for each database)\n", "db_credentials = {\n", " \"environments\": {\n", " \"dev\": {\n", " \"databases\": [\n", " {\"username\": \"dev_primary_user\", \"password\": \"dev_primary_secret\"},\n", " {\"username\": \"dev_replica_user\", \"password\": \"dev_replica_secret\"}\n", " ]\n", " },\n", " \"prod\": {\n", " \"databases\": [\n", " {\"username\": \"prod_primary_user\", \"password\": \"prod_primary_secret\"},\n", " {\"username\": \"prod_replica_user\", \"password\": \"prod_replica_secret_1\"},\n", " {\"username\": \"prod_replica_user\", \"password\": \"prod_replica_secret_2\"}\n", " ]\n", " }\n", " }\n", "}\n", "\n", "merged_db_config = deep_merge(db_topology, db_credentials)\n", "print(\"🗄️ Complete database configuration:\")\n", "jprint(merged_db_config)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Error Handling & Troubleshooting\n", "\n", "Understanding when and why `deep_merge` fails helps you design better configuration structures:\n", "\n", "### 1. List Length Mismatches\n", "\n", "**Problem:** Lists must have the same length to maintain positional relationships." ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "❌ Error: list length mismatch: path = '.users'\n" ] } ], "source": [ "# ❌ This will fail - different number of items\n", "try:\n", " config_with_mismatch = {\n", " \"users\": [\n", " {\"username\": \"alice\"},\n", " {\"username\": \"bob\"},\n", " {\"username\": \"charlie\"} # 3 users\n", " ]\n", " }\n", " \n", " secrets_with_mismatch = {\n", " \"users\": [\n", " {\"password\": \"alice_pwd\"},\n", " {\"password\": \"bob_pwd\"} # Only 2 passwords!\n", " ]\n", " }\n", " \n", " deep_merge(config_with_mismatch, secrets_with_mismatch)\n", " \n", "except ValueError as e:\n", " print(f\"❌ Error: {e}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Solution:** Ensure lists have matching lengths:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Fixed - all users have passwords:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"users\": [\n",
       "    {\n",
       "      \"username\": \"alice\",\n",
       "      \"password\": \"alice_pwd\"\n",
       "    },\n",
       "    {\n",
       "      \"username\": \"bob\",\n",
       "      \"password\": \"bob_pwd\"\n",
       "    },\n",
       "    {\n",
       "      \"username\": \"charlie\",\n",
       "      \"password\": \"charlie_pwd\"\n",
       "    }\n",
       "  ]\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"users\"\u001b[0m: \u001b[1m[\u001b[0m\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"alice\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"alice_pwd\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"bob\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"bob_pwd\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"username\"\u001b[0m: \u001b[32m\"charlie\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"charlie_pwd\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m]\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ✅ Fixed - same number of items\n", "config_fixed = {\n", " \"users\": [\n", " {\"username\": \"alice\"},\n", " {\"username\": \"bob\"},\n", " {\"username\": \"charlie\"}\n", " ]\n", "}\n", "\n", "secrets_fixed = {\n", " \"users\": [\n", " {\"password\": \"alice_pwd\"},\n", " {\"password\": \"bob_pwd\"},\n", " {\"password\": \"charlie_pwd\"} # Now we have 3 passwords\n", " ]\n", "}\n", "\n", "result = deep_merge(config_fixed, secrets_fixed)\n", "print(\"✅ Fixed - all users have passwords:\")\n", "jprint(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 2. Type Incompatibility\n", "\n", "**Problem:** Can't merge different data types (string with dict, etc.)." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "❌ Error: type of value at '.database' in data1 and data2 has to be both dict or list of dict to merge! they are and .\n" ] } ], "source": [ "# ❌ This will fail - trying to merge incompatible types\n", "try:\n", " incompatible_config = {\n", " \"database\": \"simple_connection_string\" # String\n", " }\n", " \n", " incompatible_secrets = {\n", " \"database\": {\"password\": \"secret\"} # Dict\n", " }\n", " \n", " deep_merge(incompatible_config, incompatible_secrets)\n", " \n", "except TypeError as e:\n", " print(f\"❌ Error: {e}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Solution:** Ensure compatible data structures:" ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Fixed - compatible types:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"database\": {\n",
       "    \"connection_string\": \"postgresql://host:port/db\",\n",
       "    \"password\": \"secret\"\n",
       "  }\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"database\"\u001b[0m: \u001b[1m{\u001b[0m\n", " \u001b[32m\"connection_string\"\u001b[0m: \u001b[32m\"postgresql://host:port/db\"\u001b[0m,\n", " \u001b[32m\"password\"\u001b[0m: \u001b[32m\"secret\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ✅ Fixed - both are dictionaries\n", "compatible_config = {\n", " \"database\": {\"connection_string\": \"postgresql://host:port/db\"} # Dict\n", "}\n", "\n", "compatible_secrets = {\n", " \"database\": {\"password\": \"secret\"} # Dict\n", "}\n", "\n", "result = deep_merge(compatible_config, compatible_secrets)\n", "print(\"✅ Fixed - compatible types:\")\n", "jprint(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Non-Dict Items in Lists\n", "\n", "**Problem:** List items must be dictionaries to merge properly." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "❌ Error: items in '.ports' are not dict, so you cannot merge them!\n" ] } ], "source": [ "# ❌ This will fail - list contains non-dict items\n", "try:\n", " config_with_scalars = {\n", " \"ports\": [8001, 8002, 8003] # Numbers, not dicts\n", " }\n", " \n", " secrets_with_scalars = {\n", " \"ports\": [9001, 9002, 9003] # Numbers, not dicts\n", " }\n", " \n", " deep_merge(config_with_scalars, secrets_with_scalars)\n", " \n", "except TypeError as e:\n", " print(f\"❌ Error: {e}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Solution:** Use dictionaries in lists when merging is needed:" ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Fixed - dictionaries in lists:\n" ] }, { "data": { "text/html": [ "
{\n",
       "  \"services\": [\n",
       "    {\n",
       "      \"name\": \"api\",\n",
       "      \"port\": 8001,\n",
       "      \"api_key\": \"api_secret\"\n",
       "    },\n",
       "    {\n",
       "      \"name\": \"worker\",\n",
       "      \"port\": 8002,\n",
       "      \"api_key\": \"worker_secret\"\n",
       "    },\n",
       "    {\n",
       "      \"name\": \"scheduler\",\n",
       "      \"port\": 8003,\n",
       "      \"api_key\": \"scheduler_secret\"\n",
       "    }\n",
       "  ]\n",
       "}\n",
       "
\n" ], "text/plain": [ "\u001b[1m{\u001b[0m\n", " \u001b[32m\"services\"\u001b[0m: \u001b[1m[\u001b[0m\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"api\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m8001\u001b[0m,\n", " \u001b[32m\"api_key\"\u001b[0m: \u001b[32m\"api_secret\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"worker\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m8002\u001b[0m,\n", " \u001b[32m\"api_key\"\u001b[0m: \u001b[32m\"worker_secret\"\u001b[0m\n", " \u001b[1m}\u001b[0m,\n", " \u001b[1m{\u001b[0m\n", " \u001b[32m\"name\"\u001b[0m: \u001b[32m\"scheduler\"\u001b[0m,\n", " \u001b[32m\"port\"\u001b[0m: \u001b[1;36m8003\u001b[0m,\n", " \u001b[32m\"api_key\"\u001b[0m: \u001b[32m\"scheduler_secret\"\u001b[0m\n", " \u001b[1m}\u001b[0m\n", " \u001b[1m]\u001b[0m\n", "\u001b[1m}\u001b[0m\n" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# ✅ Fixed - use dicts in lists\n", "config_with_dicts = {\n", " \"services\": [\n", " {\"name\": \"api\", \"port\": 8001},\n", " {\"name\": \"worker\", \"port\": 8002},\n", " {\"name\": \"scheduler\", \"port\": 8003}\n", " ]\n", "}\n", "\n", "secrets_with_dicts = {\n", " \"services\": [\n", " {\"api_key\": \"api_secret\"},\n", " {\"api_key\": \"worker_secret\"},\n", " {\"api_key\": \"scheduler_secret\"}\n", " ]\n", "}\n", "\n", "result = deep_merge(config_with_dicts, secrets_with_dicts)\n", "print(\"✅ Fixed - dictionaries in lists:\")\n", "jprint(result)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Best Practices\n", "\n", "### 1. Configuration File Organization\n", "\n", "**Recommended structure:**\n", "\n", "```\n", "project/\n", "├── config/\n", "│ ├── base.json # ✅ Safe to commit\n", "│ ├── environments/\n", "│ │ ├── dev.json # ✅ Safe to commit \n", "│ │ ├── staging.json # ✅ Safe to commit\n", "│ │ └── prod.json # ✅ Safe to commit\n", "│ └── secrets/ # ❌ Never commit this folder!\n", "│ ├── dev-secrets.json # ❌ Add to .gitignore\n", "│ ├── staging-secrets.json\n", "│ └── prod-secrets.json\n", "└── .gitignore # Must include config/secrets/\n", "```\n", "\n", "### 2. Validation Before Merging" ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "✅ Configuration structure validation passed\n" ] } ], "source": [ "def validate_config_structure(base_config, secrets_config):\n", " \"\"\"Validate configs have compatible structure before merging\"\"\"\n", " \n", " def validate_list_lengths(base, secrets, path=\"\"):\n", " for key in base.keys() & secrets.keys():\n", " current_path = f\"{path}.{key}\" if path else key\n", " base_val, secret_val = base[key], secrets[key]\n", " \n", " if isinstance(base_val, list) and isinstance(secret_val, list):\n", " if len(base_val) != len(secret_val):\n", " raise ValueError(\n", " f\"List length mismatch at {current_path}: \"\n", " f\"base has {len(base_val)} items, secrets has {len(secret_val)} items\"\n", " )\n", " \n", " elif isinstance(base_val, dict) and isinstance(secret_val, dict):\n", " validate_list_lengths(base_val, secret_val, current_path)\n", " \n", " validate_list_lengths(base_config, secrets_config)\n", " print(\"✅ Configuration structure validation passed\")\n", "\n", "# Example usage\n", "base = {\"users\": [{\"username\": \"alice\"}, {\"username\": \"bob\"}]}\n", "secrets = {\"users\": [{\"password\": \"pwd1\"}, {\"password\": \"pwd2\"}]}\n", "\n", "validate_config_structure(base, secrets)\n", "result = deep_merge(base, secrets)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 3. Environment-Specific Loading" ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [], "source": [ "import os\n", "import json\n", "\n", "def load_configuration(environment: str):\n", " \"\"\"Load and merge configuration for specific environment\"\"\"\n", " \n", " # Load base configuration\n", " with open(f\"config/environments/{environment}.json\") as f:\n", " base_config = json.load(f)\n", " \n", " # Load secrets (from secure location, not git)\n", " secrets_path = f\"config/secrets/{environment}-secrets.json\"\n", " if os.path.exists(secrets_path):\n", " with open(secrets_path) as f:\n", " secrets_config = json.load(f)\n", " else:\n", " print(f\"⚠️ No secrets file found at {secrets_path}\")\n", " secrets_config = {}\n", " \n", " # Merge and return\n", " return deep_merge(base_config, secrets_config)\n", "\n", "# Usage\n", "# config = load_configuration(\"prod\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### 4. CI/CD Integration" ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [], "source": [ "# Example: Inject secrets at deployment time\n", "def prepare_deployment_config(base_config_path: str, environment: str):\n", " \"\"\"Prepare config for deployment by injecting secrets from environment variables\"\"\"\n", " \n", " with open(base_config_path) as f:\n", " base_config = json.load(f)\n", " \n", " # Build secrets from environment variables\n", " secrets = {\n", " \"database\": {\n", " \"username\": os.environ[\"DB_USERNAME\"],\n", " \"password\": os.environ[\"DB_PASSWORD\"]\n", " },\n", " \"api_keys\": {\n", " \"stripe\": os.environ[\"STRIPE_API_KEY\"],\n", " \"sendgrid\": os.environ[\"SENDGRID_API_KEY\"]\n", " }\n", " }\n", " \n", " return deep_merge(base_config, secrets)\n", "\n", "# In your deployment script:\n", "# deployment_config = prepare_deployment_config(\"config/prod.json\", \"prod\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "---\n", "\n", "## Summary\n", "\n", "The `deep_merge` pattern provides a **secure, scalable solution** for configuration management:\n", "\n", "### 🎯 **Key Benefits**\n", "- **🔒 Security**: Keeps secrets out of version control\n", "- **🧠 Intelligence**: Structure-aware merging preserves relationships \n", "- **🛡️ Safety**: Immutable operations prevent accidental data corruption\n", "- **📈 Scalability**: Handles complex, deeply nested configurations\n", "- **🔍 Validation**: Clear error messages for troubleshooting\n", "\n", "### 🚀 **When to Use This Pattern**\n", "- ✅ Multi-environment applications (dev/staging/prod)\n", "- ✅ Microservice configurations with shared structure\n", "- ✅ CI/CD pipelines that inject secrets at deployment\n", "- ✅ Applications requiring compliance with security standards\n", "- ✅ Teams that need to separate config ownership\n", "\n", "### 💡 **Next Steps**\n", "1. **Identify** configuration vs secrets in your application\n", "2. **Separate** them into different files/sources\n", "3. **Structure** your configs with compatible schemas\n", "4. **Merge** them safely with `deep_merge`\n", "5. **Automate** the process in your deployment pipeline\n", "\n", "---\n", "\n", "**Remember**: Configuration management is a security practice, not just a convenience. Use `deep_merge` to build robust, secure applications that scale with your team and infrastructure needs." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.11.8" } }, "nbformat": 4, "nbformat_minor": 4 }