{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Getting Aircraft Configuration Data"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Successfully saved 20 documents to ../data/examples/aircraft_configuration/aircraft_configuration.json\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "from pymongo import MongoClient\n",
    "from bson.json_util import dumps\n",
    "\n",
    "# 1. Setup Connection\n",
    "client = MongoClient(\"mongodb+srv://ioc_de_ete:RjL7xac3g2U6RULZ@ioc-de-ete.oijg8.mongodb.net/?authMechanism=SCRAM-SHA-1&tls=true\") # Replace with your connection string\n",
    "db = client['external']\n",
    "collection = db['replica_nlo_aircraft_configuration_serve']\n",
    "\n",
    "# 2. Query the data (Get first 20)\n",
    "# We use list() to pull the data from the cursor immediately\n",
    "cursor = collection.find().limit(20)\n",
    "documents_list = list(cursor)\n",
    "\n",
    "# 3. Save to JSON file\n",
    "output_filename = '../data/examples/aircraft_configuration/aircraft_configuration.json'\n",
    "\n",
    "# OPTION A: Using bson.json_util (Recommended)\n",
    "# This handles ObjectId and Datetime fields automatically\n",
    "with open(output_filename, 'w') as json_file:\n",
    "    json_file.write(dumps(documents_list, indent=4))\n",
    "\n",
    "print(f\"Successfully saved {len(documents_list)} documents to {output_filename}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Complete PyCharter Workflow: Aircraft Configuration (MongoDB)\n",
    "\n",
    "This notebook demonstrates the complete workflow for Aircraft Configuration using MongoDB:\n",
    "1. Convert Pydantic model to JSON Schema\n",
    "2. Parse contract YAML and decompose into components\n",
    "3. Store all components in MongoDB metadata store\n",
    "4. Extract from store and reconstruct contract\n",
    "5. Generate Pydantic model from reconstructed contract\n",
    "\n",
    "## Prerequisites\n",
    "\n",
    "Before running this notebook, ensure you have:\n",
    "1. MongoDB running and accessible at `localhost:27017`\n",
    "2. MongoDB credentials: `rootUser` / `rootPassword`\n",
    "3. `pymongo` installed: `pip install pymongo`\n",
    "4. The aircraft configuration contract YAML file available\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "✓ Loaded 20 records from JSON file\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "import os\n",
    "from typing import Optional, Dict, Any, List\n",
    "from datetime import datetime\n",
    "import sys\n",
    "\n",
    "from pydantic import BaseModel\n",
    "\n",
    "from pycharter import (\n",
    "    # Service 1: Contract Parser\n",
    "    parse_contract_file,\n",
    "    ContractMetadata,\n",
    "    # Service 2: Metadata Store\n",
    "    MongoDBMetadataStore,\n",
    "    # Service 3: Pydantic Generator\n",
    "    from_dict,\n",
    "    # Service 4: JSON Schema Converter\n",
    "    to_dict,\n",
    "    # Service 5: Runtime Validator\n",
    "    get_model_from_contract,\n",
    "    # Service 6: Contract Builder\n",
    "    build_contract_from_store,\n",
    ")\n",
    "\n",
    "# Setup paths\n",
    "ROOT_DIR = os.path.join('..')\n",
    "DATA_DIR = os.path.join(ROOT_DIR, 'data')\n",
    "EXAMPLES_DIR = os.path.join(DATA_DIR, 'examples')\n",
    "AIRCRAFT_CONFIGURATION_DIR = os.path.join(EXAMPLES_DIR, 'aircraft_configuration')\n",
    "AIRCRAFT_CONFIGURATION_CONTRACT = os.path.join(AIRCRAFT_CONFIGURATION_DIR, 'aircraft_configuration_contract.yaml')\n",
    "AIRCRAFT_CONFIGURATION_JSON = os.path.join(AIRCRAFT_CONFIGURATION_DIR, 'aircraft_configuration.json')\n",
    "\n",
    "# Add paths for importing aircraft_configuration models\n",
    "sys.path.append(ROOT_DIR)\n",
    "sys.path.append(AIRCRAFT_CONFIGURATION_DIR)\n",
    "\n",
    "# Import AircraftConfigurationApi model\n",
    "from aircraft_configuration_models import AircraftConfigurationApi\n",
    "\n",
    "# Helper function to convert MongoDB-style JSON to Python objects\n",
    "def convert_mongodb_json(obj):\n",
    "    \"\"\"Convert MongoDB-style JSON ($oid, $date) to Python objects.\"\"\"\n",
    "    if isinstance(obj, dict):\n",
    "        if '$oid' in obj:\n",
    "            return obj['$oid']  # Return as string, or convert to ObjectId if needed\n",
    "        elif '$date' in obj:\n",
    "            # Convert ISO date string to datetime\n",
    "            date_str = obj['$date']\n",
    "            if isinstance(date_str, str):\n",
    "                # Handle ISO format: \"2014-01-01T00:00:00Z\"\n",
    "                try:\n",
    "                    return datetime.fromisoformat(date_str.replace('Z', '+00:00'))\n",
    "                except:\n",
    "                    return date_str\n",
    "            return date_str\n",
    "        else:\n",
    "            return {k: convert_mongodb_json(v) for k, v in obj.items()}\n",
    "    elif isinstance(obj, list):\n",
    "        return [convert_mongodb_json(item) for item in obj]\n",
    "    else:\n",
    "        return obj\n",
    "\n",
    "# Load sample data from JSON file\n",
    "json_data = None\n",
    "if os.path.exists(AIRCRAFT_CONFIGURATION_JSON):\n",
    "    with open(AIRCRAFT_CONFIGURATION_JSON, 'r') as f:\n",
    "        raw_data = json.load(f)\n",
    "        # Convert MongoDB-style JSON to Python objects\n",
    "        json_data = convert_mongodb_json(raw_data)\n",
    "    print(f\"✓ Loaded {len(json_data)} records from JSON file\")\n",
    "else:\n",
    "    print(\"⚠ JSON file not found, will use sample data from model\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "======================================================================\n",
      "Step 1: Convert Pydantic Model to JSON Schema\n",
      "======================================================================\n",
      "✓ Converted AircraftConfigurationApi model to JSON Schema\n",
      "  Schema title: AircraftConfigurationApi\n",
      "  Schema version: 1.0.0\n",
      "  Number of properties: 57\n"
     ]
    }
   ],
   "source": [
    "# Step 1: Convert Pydantic Model to JSON Schema\n",
    "print(\"=\" * 70)\n",
    "print(\"Step 1: Convert Pydantic Model to JSON Schema\")\n",
    "print(\"=\" * 70)\n",
    "\n",
    "# Convert AircraftConfigurationApi Pydantic model to JSON Schema\n",
    "aircraft_configuration_schema = to_dict(AircraftConfigurationApi)\n",
    "print(f\"✓ Converted AircraftConfigurationApi model to JSON Schema\")\n",
    "print(f\"  Schema title: {aircraft_configuration_schema.get('title')}\")\n",
    "print(f\"  Schema version: {aircraft_configuration_schema.get('version', 'N/A')}\")\n",
    "print(f\"  Number of properties: {len(aircraft_configuration_schema.get('properties', {}))}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "======================================================================\n",
      "Step 2: Parse Contract YAML and Decompose into Components\n",
      "======================================================================\n",
      "✓ Parsed contract file: ../data/examples/aircraft_configuration/aircraft_configuration_contract.yaml\n",
      "  Schema version: 1.0.0\n",
      "  Metadata version: 1.0.0\n",
      "  Ownership: operations-team / data-engineering\n",
      "  Coercion rules: 40 fields\n",
      "  Validation rules: 34 fields\n",
      "  Governance rules: Yes\n"
     ]
    }
   ],
   "source": [
    "# Step 2: Parse Contract YAML and Decompose into Components\n",
    "print(\"\\n\" + \"=\" * 70)\n",
    "print(\"Step 2: Parse Contract YAML and Decompose into Components\")\n",
    "print(\"=\" * 70)\n",
    "\n",
    "# Parse the contract file\n",
    "contract_metadata = parse_contract_file(AIRCRAFT_CONFIGURATION_CONTRACT)\n",
    "print(f\"✓ Parsed contract file: {AIRCRAFT_CONFIGURATION_CONTRACT}\")\n",
    "\n",
    "# Extract components\n",
    "schema = contract_metadata.schema\n",
    "metadata = contract_metadata.metadata\n",
    "ownership = contract_metadata.ownership\n",
    "governance_rules = contract_metadata.governance_rules\n",
    "versions = contract_metadata.versions\n",
    "\n",
    "print(f\"  Schema version: {versions.get('schema', 'N/A')}\")\n",
    "print(f\"  Metadata version: {versions.get('metadata', 'N/A')}\")\n",
    "print(f\"  Ownership: {ownership.get('owner', 'N/A')} / {ownership.get('team', 'N/A')}\")\n",
    "\n",
    "# Extract coercion and validation rules from schema properties\n",
    "# (They're embedded in the schema properties in the contract YAML)\n",
    "coercion_rules = {}\n",
    "validation_rules = {}\n",
    "\n",
    "if \"properties\" in schema:\n",
    "    for field_name, field_schema in schema[\"properties\"].items():\n",
    "        if isinstance(field_schema, dict):\n",
    "            # Extract coercion rule\n",
    "            if \"coercion\" in field_schema:\n",
    "                coercion_rules[field_name] = field_schema[\"coercion\"]\n",
    "            \n",
    "            # Extract validation rules\n",
    "            if \"validations\" in field_schema:\n",
    "                validation_rules[field_name] = field_schema[\"validations\"]\n",
    "\n",
    "print(f\"  Coercion rules: {len(coercion_rules)} fields\")\n",
    "print(f\"  Validation rules: {len(validation_rules)} fields\")\n",
    "print(f\"  Governance rules: {'Yes' if governance_rules else 'No'}\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "======================================================================\n",
      "Step 3: Store All Components in MongoDB Metadata Store\n",
      "======================================================================\n",
      "✓ Connected to MongoDB metadata store (database: pycharter)\n",
      "✓ Stored schema (ID: 693231e2cf9a5bd1072e8e2e, version: 1.0.0)\n",
      "✓ Stored metadata\n",
      "✓ Stored metadata with ownership and governance rules\n",
      "✓ Stored coercion rules (version: 1.0.0)\n",
      "✓ Stored validation rules (version: 1.0.0)\n",
      "\n",
      "✓ All components stored successfully (Schema ID: 693231e2cf9a5bd1072e8e2e)\n"
     ]
    }
   ],
   "source": [
    "# Step 3: Store All Components in MongoDB Metadata Store\n",
    "print(\"\\n\" + \"=\" * 70)\n",
    "print(\"Step 3: Store All Components in MongoDB Metadata Store\")\n",
    "print(\"=\" * 70)\n",
    "\n",
    "# Connect to MongoDB\n",
    "connection_string = \"mongodb://rootUser:rootPassword@localhost:27017/\"\n",
    "database_name = \"pycharter\"  # Optional: defaults to \"pycharter\"\n",
    "store = MongoDBMetadataStore(\n",
    "    connection_string=connection_string,\n",
    "    database_name=database_name\n",
    ")\n",
    "store.connect()\n",
    "print(f\"✓ Connected to MongoDB metadata store (database: {database_name})\")\n",
    "\n",
    "# Store schema (use the schema from the contract, which has version info)\n",
    "schema_version = versions.get(\"schema\", schema.get(\"version\", \"1.0.0\"))\n",
    "schema_id = store.store_schema(\n",
    "    schema_name=\"aircraft_configuration\",\n",
    "    schema=schema,\n",
    "    version=schema_version\n",
    ")\n",
    "print(f\"✓ Stored schema (ID: {schema_id}, version: {schema_version})\")\n",
    "\n",
    "# Store metadata\n",
    "if metadata:\n",
    "    store.store_metadata(\n",
    "        resource_id=schema_id,\n",
    "        resource_type=\"schema\",\n",
    "        metadata=metadata\n",
    "    )\n",
    "    print(f\"✓ Stored metadata\")\n",
    "\n",
    "# Store ownership and governance rules via metadata\n",
    "metadata_dict = metadata.copy() if metadata else {}\n",
    "if ownership:\n",
    "    metadata_dict[\"business_owners\"] = [ownership.get(\"owner\", \"unknown\")] if ownership.get(\"owner\") else []\n",
    "if governance_rules:\n",
    "    metadata_dict[\"governance_rules\"] = governance_rules\n",
    "\n",
    "if metadata_dict:\n",
    "    store.store_metadata(\n",
    "        resource_id=schema_id,\n",
    "        resource_type=\"schema\",\n",
    "        metadata=metadata_dict\n",
    "    )\n",
    "    print(f\"✓ Stored metadata with ownership and governance rules\")\n",
    "\n",
    "# Store coercion rules\n",
    "if coercion_rules:\n",
    "    coercion_version = versions.get(\"coercion_rules\", \"1.0.0\")\n",
    "    store.store_coercion_rules(\n",
    "        schema_id=schema_id,\n",
    "        coercion_rules=coercion_rules,\n",
    "        version=coercion_version\n",
    "    )\n",
    "    print(f\"✓ Stored coercion rules (version: {coercion_version})\")\n",
    "\n",
    "# Store validation rules\n",
    "if validation_rules:\n",
    "    validation_version = versions.get(\"validation_rules\", \"1.0.0\")\n",
    "    store.store_validation_rules(\n",
    "        schema_id=schema_id,\n",
    "        validation_rules=validation_rules,\n",
    "        version=validation_version\n",
    "    )\n",
    "    print(f\"✓ Stored validation rules (version: {validation_version})\")\n",
    "\n",
    "print(f\"\\n✓ All components stored successfully (Schema ID: {schema_id})\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 4: Extract from Metadata Store and Reconstruct Contract\n",
    "print(\"\\n\" + \"=\" * 70)\n",
    "print(\"Step 4: Extract from Metadata Store and Reconstruct Contract\")\n",
    "print(\"=\" * 70)\n",
    "\n",
    "# Reconstruct the contract from the metadata store\n",
    "reconstructed_contract = build_contract_from_store(\n",
    "    store=store,\n",
    "    schema_id=schema_id,\n",
    "    version=None,  # Use latest version\n",
    "    include_metadata=True,\n",
    "    include_ownership=True,\n",
    "    include_governance=True\n",
    ")\n",
    "print(f\"✓ Reconstructed contract from metadata store\")\n",
    "print(f\"  Contract keys: {list(reconstructed_contract.keys())}\")\n",
    "\n",
    "# Show what was reconstructed\n",
    "if \"schema\" in reconstructed_contract:\n",
    "    print(f\"  Schema title: {reconstructed_contract['schema'].get('title')}\")\n",
    "    print(f\"  Schema version: {reconstructed_contract['schema'].get('version')}\")\n",
    "if \"metadata\" in reconstructed_contract:\n",
    "    print(f\"  Metadata present: Yes\")\n",
    "if \"ownership\" in reconstructed_contract:\n",
    "    print(f\"  Ownership: {reconstructed_contract['ownership'].get('owner', 'N/A')}\")\n",
    "if \"governance_rules\" in reconstructed_contract:\n",
    "    print(f\"  Governance rules present: Yes\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Step 5: Create Pydantic Model from Reconstructed Contract\n",
    "print(\"\\n\" + \"=\" * 70)\n",
    "print(\"Step 5: Create Pydantic Model from Reconstructed Contract\")\n",
    "print(\"=\" * 70)\n",
    "\n",
    "# Generate Pydantic model from the reconstructed contract\n",
    "# This will merge coercion and validation rules into the schema\n",
    "AircraftConfigurationModel = get_model_from_contract(\n",
    "    contract=reconstructed_contract,\n",
    "    model_name=\"AircraftConfiguration\"\n",
    ")\n",
    "print(f\"✓ Generated Pydantic model: {AircraftConfigurationModel.__name__}\")\n",
    "\n",
    "# Test the model with sample data (if available)\n",
    "if df is not None and len(df) > 0:\n",
    "    print(\"\\nTesting model with sample data:\")\n",
    "    sample_data = df.iloc[0].to_dict()\n",
    "    try:\n",
    "        # Clean up the data - handle NaN values and convert datetime objects\n",
    "        cleaned_data = {}\n",
    "        for key, value in sample_data.items():\n",
    "            # Convert column names to lowercase (matching schema property names)\n",
    "            field_name = key.lower() if isinstance(key, str) else key\n",
    "            \n",
    "            if pd.isna(value):\n",
    "                cleaned_data[field_name] = None\n",
    "            elif isinstance(value, pd.Timestamp):\n",
    "                cleaned_data[field_name] = value.isoformat()\n",
    "            elif isinstance(value, (int, float)) and pd.isna(value):\n",
    "                cleaned_data[field_name] = None\n",
    "            else:\n",
    "                cleaned_data[field_name] = value\n",
    "        \n",
    "        # Use the runtime validator to validate and coerce the data\n",
    "        from pycharter import validate_with_contract\n",
    "        \n",
    "        validation_result = validate_with_contract(\n",
    "            data=cleaned_data,\n",
    "            contract=reconstructed_contract\n",
    "        )\n",
    "        \n",
    "        if validation_result.is_valid:\n",
    "            aircraft_configuration_instance = AircraftConfigurationModel(**validation_result.validated_data)\n",
    "            print(f\"✓ Successfully created AircraftConfiguration instance\")\n",
    "            print(f\"  Registration: {aircraft_configuration_instance.registration}\")\n",
    "            print(f\"  AC Operator: {aircraft_configuration_instance.ac_operator}\")\n",
    "            print(f\"  AC Owner: {aircraft_configuration_instance.ac_owner}\")\n",
    "        else:\n",
    "            print(f\"⚠ Validation issues found:\")\n",
    "            for error in validation_result.errors[:5]:  # Show first 5 errors\n",
    "                print(f\"  - {error}\")\n",
    "            if len(validation_result.errors) > 5:\n",
    "                print(f\"  ... and {len(validation_result.errors) - 5} more errors\")\n",
    "    except Exception as e:\n",
    "        print(f\"✗ Error creating instance: {e}\")\n",
    "        print(f\"  Error details: {type(e).__name__}\")\n",
    "        import traceback\n",
    "        traceback.print_exc()\n",
    "else:\n",
    "    print(\"\\n⚠ No sample data available for testing\")\n",
    "\n",
    "# Disconnect from store\n",
    "store.disconnect()\n",
    "print(\"\\n✓ Disconnected from metadata store\")\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# View the generated model schema\n",
    "AircraftConfigurationModel.model_json_schema()\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python (pycharter-dev)",
   "language": "python",
   "name": "pycharter-dev"
  },
  "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.12.7"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
