#!/usr/bin/env python3 """ Demo Data Loader for Query Orchestration API This script imports demo data from a CSV file into the Query Orchestration API. It creates a demo client, uploads unique PDF documents for each file in the CSV, and creates field extraction records with both single-value and array fields. The CSV is expected to have: - One header row with column names - Multiple data rows where rows with the same FILE_NAME belong to the same document - Single-value fields (columns 1-18) that are identical for rows with the same FILE_NAME - Array fields (columns 19+) that vary per row, forming multiple array items per document Usage: python3 demo_data_loader.py [jwt_token] Arguments: csv_path - Path to the CSV file containing demo data base_url - Base URL of the API (e.g., http://localhost:8080) jwt_token - Optional JWT token for authentication (placeholder for future use) Example: python3 demo_data_loader.py SaaS_Demo_Data_Sam.csv http://localhost:8080 """ import csv import json import sys import time import urllib.request import urllib.error import urllib.parse from collections import defaultdict from datetime import datetime from typing import Dict, List, Optional, Any, Tuple # Column indices for single-value fields (0-17, corresponding to columns 1-18 in CSV) SINGLE_FIELD_COLUMNS = { 0: "fileName", # FILE_NAME 1: "contractTitle", # CONTRACT_TITLE 2: "aareteDerivedAmendmentNum", # AARETE_DERIVED_AMENDMENT_NUM 3: "clientName", # CLIENT_NAME 4: "payerName", # PAYER_NAME 5: "payerState", # PAYER_STATE 6: "providerState", # PROVIDER_STATE 7: "filenameTin", # FILENAME_TIN 8: "provGroupTin", # PROV_GROUP_TIN 9: "provGroupNpi", # PROV_GROUP_NPI 10: "provGroupNameFull", # PROV_GROUP_NAME_FULL 11: "provOtherTin", # PROV_OTHER_TIN 12: "provOtherNpi", # PROV_OTHER_NPI 13: "provOtherNameFull", # PROV_OTHER_NAME_FULL 14: "aareteDerivedEffectiveDt", # AARETE_DERIVED_EFFECTIVE_DT 15: "aareteDerivedTerminationDt",# AARETE_DERIVED_TERMINATION_DT 16: "autoRenewalInd", # AUTO_RENEWAL_IND 17: "autoRenewalTerm", # AUTO_RENEWAL_TERM } # Column indices for array fields (18+, corresponding to columns 19+ in CSV) ARRAY_FIELD_COLUMNS = { 18: "exhibitTitle", # EXHIBIT_TITLE 19: "exhibitPage", # EXHIBIT_PAGE 20: "reimbProvTin", # REIMB_PROV_TIN 21: "reimbProvNpi", # REIMB_PROV_NPI 22: "reimbProvName", # REIMB_PROV_NAME 23: "reimbEffectiveDt", # REIMB_EFFECTIVE_DT 24: "reimbTerminationDt", # REIMB_TERMINATION_DT 25: "aareteDerivedClaimTypeCd", # AARETE_DERIVED_CLAIM_TYPE_CD 26: "aareteDerivedProduct", # AARETE_DERIVED_PRODUCT 27: "aareteDerivedLob", # AARETE_DERIVED_LOB 28: "aareteDerivedProgram", # AARETE_DERIVED_PROGRAM 29: "aareteDerivedNetwork", # AARETE_DERIVED_NETWORK 30: "lobProgramRelationship", # LOB_PROGRAM_RELATIONSHIP 31: "lobProductRelationship", # LOB_PRODUCT_RELATIONSHIP 32: "aareteDerivedProvType", # AARETE_DERIVED_PROV_TYPE 33: "provTaxonomyCd", # PROV_TAXONOMY_CD 34: "provTaxonomyCdDesc", # PROV_TAXONOMY_CD_DESC 35: "provSpecialtyCd", # PROV_SPECIALTY_CD 36: "provSpecialtyCdDesc", # PROV_SPECIALTY_CD_DESC 37: "placeOfServiceCd", # PLACE_OF_SERVICE_CD 38: "placeOfServiceCdDesc", # PLACE_OF_SERVICE_CD_DESC 39: "billTypeCd", # BILL_TYPE_CD 40: "billTypeCdDesc", # BILL_TYPE_CD_DESC 41: "patientAgeMin", # PATIENT_AGE_MIN 42: "patientAgeMax", # PATIENT_AGE_MAX 43: "reimbTerm", # REIMB_TERM 44: "carveoutInd", # CARVEOUT_IND 45: "carveoutCd", # CARVEOUT_CD 46: "lesserOfInd", # LESSER_OF_IND 47: "greaterOfInd", # GREATER_OF_IND 48: "aareteDerivedReimbMethod", # AARETE_DERIVED_REIMB_METHOD 49: "unitOfMeasure", # UNIT_OF_MEASURE 50: "reimbPctRate", # REIMB_PCT_RATE 51: "reimbFeeRate", # REIMB_FEE_RATE 52: "reimbConversionFactor", # REIMB_CONVERSION_FACTOR 53: "triggerCapThresholdAmt", # TRIGGER_CAP_THRESHOLD_AMT 54: "triggerBaseThreshold", # TRIGGER_BASE_THRESHOLD 55: "defaultInd", # DEFAULT_IND 56: "additionDesc", # ADDITION_DESC 57: "additionMaxPctRateInc", # ADDITION_MAX_PCT_RATE_INC 58: "additionMaxFeeRateInc", # ADDITION_MAX_FEE_RATE_INC 59: "aareteDerivedAdditionRateChangeTimeline", # AARETE_DERIVED_ADDITION_RATE_CHANGE_TIMELINE 60: "aareteDerivedFeeSchedule", # AARETE_DERIVED_FEE_SCHEDULE 61: "aareteDerivedFeeScheduleVersion", # AARETE_DERIVED_FEE_SCHEDULE_VERSION 62: "serviceTerm", # SERVICE_TERM 63: "cpt4ProcCd", # CPT4_PROC_CD 64: "cpt4ProcCdDesc", # CPT4_PROC_CD_DESC 65: "cpt4ProcMod", # CPT4_PROC_MOD 66: "cpt4ProcModDesc", # CPT4_PROC_MOD_DESC 67: "revenueCd", # REVENUE_CD 68: "revenueCdDesc", # REVENUE_CD_DESC 69: "diagCd", # DIAG_CD 70: "diagCdDesc", # DIAG_CD_DESC 71: "ndcCd", # NDC_CD 72: "ndcCdDesc", # NDC_CD_DESC 73: "claimAdmitTypeCd", # CLAIM_ADMIT_TYPE_CD 74: "authAdmitTypeDesc", # AUTH_ADMIT_TYPE_DESC 75: "claimStatusCd", # CLAIM_STATUS_CD 76: "claimStatusCdDesc", # CLAIM_STATUS_CD_DESC 77: "grouperType", # GROUPER_TYPE 78: "grouperCd", # GROUPER_CD 79: "grouperCdDesc", # GROUPER_CD_DESC 80: "grouperPctRate", # GROUPER_PCT_RATE 81: "grouperBaseRate", # GROUPER_BASE_RATE 82: "aareteDerivedGrouperVersion", # AARETE_DERIVED_GROUPER_VERSION 83: "grouperAlternativeLevelOfCare", # GROUPER_ALTERNATIVE_LEVEL_OF_CARE 84: "grouperSeverityInd", # GROUPER_SEVERITY_IND 85: "grouperSeverity", # GROUPER_SEVERITY 86: "grouperRiskOfMortalitySubclass", # GROUPER_RISK_OF_MORTALITY_SUBCLASS 87: "grouperTransferInd", # GROUPER_TRANSFER_IND 88: "grouperReadmissionsInd", # GROUPER_READMISSIONS_IND 89: "grouperHacInd", # GROUPER_HAC_IND 90: "outlierTerm", # OUTLIER_TERM 91: "outlierFirstDollarInd", # OUTLIER_FIRST_DOLLAR_IND 92: "rangeNbrDays", # RANGE_NBR_DAYS 93: "outlierFixedLossNbrDaysThreshold", # OUTLIER_FIXED_LOSS_NBR_DAYS_THRESHOLD 94: "outlierFixedLossThreshold", # OUTLIER_FIXED_LOSS_THRESHOLD 95: "outlierMaximum", # OUTLIER_MAXIMUM 96: "outlierMaximumFrequency", # OUTLIER_MAXIMUM_FREQUENCY 97: "outlierPctRate", # OUTLIER_PCT_RATE 98: "outlierExclusionCd", # OUTLIER_EXCLUSION_CD 99: "outlierExclusionCdDesc", # OUTLIER_EXCLUSION_CD_DESC 100: "facilityAdjustmentTerm", # FACILITY_ADJUSTMENT_TERM 101: "dshInd", # DSH_IND 102: "dshPctRate", # DSH_PCT_RATE 103: "dshFeeRate", # DSH_FEE_RATE 104: "imeInd", # IME_IND 105: "imePctRate", # IME_PCT_RATE 106: "imeFeeRate", # IME_FEE_RATE 107: "ntapInd", # NTAP_IND 108: "ntapPctRate", # NTAP_PCT_RATE 109: "ntapFeeRate", # NTAP_FEE_RATE 110: "ucInd", # UC_IND 111: "ucPctRate", # UC_PCT_RATE 112: "ucFeeRate", # UC_FEE_RATE 113: "gmeInd", # GME_IND 114: "gmePctRate", # GME_PCT_RATE 115: "gmeFeeRate", # GME_FEE_RATE 116: "rateEscalatorInd", # RATE_ESCALATOR_IND 117: "rateEscalatorDesc", # RATE_ESCALATOR_DESC 118: "rateEscalatorMaxRateIncPct", # RATE_ESCALATOR_MAX_RATE_INC_PCT 119: "rateEscalatorRateChangeTimeline", # RATE_ESCALATOR_RATE_CHANGE_TIMELINE 120: "stopLossTerm", # STOP_LOSS_TERM 121: "stopLossFirstDollarInd", # STOP_LOSS_FIRST_DOLLAR_IND 122: "stopLossRangeNbrDays", # STOP_LOSS_RANGE_NBR_DAYS 123: "stopLossFixedLossThreshold", # STOP_LOSS_FIXED_LOSS_THRESHOLD 124: "stopLossMaximum", # STOP_LOSS_MAXIMUM 125: "stopLossMaximumFrequency", # STOP_LOSS_MAXIMUM_FREQUENCY 126: "stopLossDailyMaxRate", # STOP_LOSS_DAILY_MAX_RATE 127: "stopLossPctRateOnExcessCharges", # STOP_LOSS_PCT_RATE_ON_EXCESS_CHARGES 128: "stopLossExclusionCd", # STOP_LOSS_EXCLUSION_CD 129: "stopLossExclusionDesc", # STOP_LOSS_EXCLUSION_DESC } # Fields that should be converted to boolean BOOLEAN_FIELDS = { "autoRenewalInd", "carveoutInd", "lesserOfInd", "greaterOfInd", "defaultInd", "grouperSeverityInd", "grouperTransferInd", "grouperReadmissionsInd", "grouperHacInd", "outlierFirstDollarInd", "dshInd", "imeInd", "ntapInd", "ucInd", "gmeInd", "rateEscalatorInd", "stopLossFirstDollarInd" } # Fields that should be converted to integer INTEGER_FIELDS = {"aareteDerivedAmendmentNum"} # Fields that should be converted to float/number NUMERIC_FIELDS = { "reimbPctRate", "reimbFeeRate", "reimbConversionFactor", "triggerCapThresholdAmt", "triggerBaseThreshold", "additionMaxPctRateInc", "additionMaxFeeRateInc", "grouperPctRate", "grouperBaseRate", "outlierFixedLossNbrDaysThreshold", "outlierFixedLossThreshold", "outlierMaximum", "outlierMaximumFrequency", "outlierPctRate", "dshPctRate", "dshFeeRate", "imePctRate", "imeFeeRate", "ntapPctRate", "ntapFeeRate", "ucPctRate", "ucFeeRate", "gmePctRate", "gmeFeeRate", "rateEscalatorMaxRateIncPct", "rateEscalatorRateChangeTimeline", "stopLossRangeNbrDays", "stopLossFixedLossThreshold", "stopLossMaximum", "stopLossMaximumFrequency", "stopLossDailyMaxRate", "stopLossPctRateOnExcessCharges" } # Fields that should be formatted as dates (YYYY-MM-DD) DATE_FIELDS = { "aareteDerivedEffectiveDt", "aareteDerivedTerminationDt", "reimbEffectiveDt", "reimbTerminationDt" } def read_token_file(filepath: str) -> Optional[str]: """ Read a JWT token from a file. Args: filepath: Path to the file containing the token. Returns: The token string (stripped of whitespace) or None if file cannot be read. """ try: with open(filepath, "r", encoding="utf-8") as f: token = f.read().strip() if token: return token return None except (IOError, OSError) as e: print(f" [ERROR] Failed to read token file: {e}") return None def print_header(msg: str) -> None: """Print a formatted section header.""" print(f"\n{'=' * 60}") print(f" {msg}") print(f"{'=' * 60}") def print_success(msg: str) -> None: """Print a success message.""" print(f" [OK] {msg}") def print_error(msg: str) -> None: """Print an error message.""" print(f" [ERROR] {msg}") def print_info(msg: str) -> None: """Print an informational message.""" print(f" {msg}") def parse_boolean(value: str) -> Optional[bool]: """ Parse a string value to boolean. Args: value: String value like 'Y', 'N', 'true', 'false', etc. Returns: Boolean value or None if empty/invalid. """ if not value or value.strip() == "": return None v = value.strip().upper() if v in ("Y", "YES", "TRUE", "1"): return True if v in ("N", "NO", "FALSE", "0"): return False return None def parse_integer(value: str) -> Optional[int]: """ Parse a string value to integer. Args: value: String representation of an integer. Returns: Integer value or None if empty/invalid. """ if not value or value.strip() == "": return None try: return int(value.strip()) except ValueError: return None def parse_numeric(value: str) -> Optional[float]: """ Parse a string value to float, handling percentages and special characters. Args: value: String representation of a number (may include % or other chars). Returns: Float value or None if empty/invalid. """ if not value or value.strip() == "": return None v = value.strip() # Remove percentage sign and other common suffixes v = v.replace("%", "").replace("$", "").replace(",", "").strip() try: return float(v) except ValueError: return None def parse_date(value: str) -> Optional[str]: """ Parse a date string and convert to YYYY-MM-DD format. Args: value: Date string in various formats (M/D/YYYY, MM/DD/YYYY, etc.) Returns: Date in YYYY-MM-DD format or None if empty/invalid. """ if not value or value.strip() == "": return None v = value.strip() # Try common date formats formats = [ "%m/%d/%Y", # 1/1/2025 or 01/01/2025 "%Y-%m-%d", # 2025-01-01 "%d/%m/%Y", # 01/01/2025 (European) "%Y/%m/%d", # 2025/01/01 ] for fmt in formats: try: dt = datetime.strptime(v, fmt) return dt.strftime("%Y-%m-%d") except ValueError: continue return None def convert_field_value(field_name: str, value: str) -> Any: """ Convert a field value to its appropriate type based on field name. Args: field_name: The API field name. value: The raw string value from CSV. Returns: Converted value in appropriate type. """ if field_name in BOOLEAN_FIELDS: return parse_boolean(value) if field_name in INTEGER_FIELDS: return parse_integer(value) if field_name in NUMERIC_FIELDS: return parse_numeric(value) if field_name in DATE_FIELDS: return parse_date(value) # Default: return as string (None if empty) if not value or value.strip() == "": return None return value.strip() def api_request( base_url: str, endpoint: str, method: str = "GET", data: Optional[Dict] = None, jwt_token: Optional[str] = None, max_retries: int = 3 ) -> Tuple[int, Any]: """ Make an HTTP request to the API with retry logic for rate limiting. Args: base_url: Base URL of the API. endpoint: API endpoint path. method: HTTP method (GET, POST, etc.) data: Request body data (will be JSON encoded). jwt_token: Optional JWT token for authorization. max_retries: Maximum number of retries for rate limiting. Returns: Tuple of (HTTP status code, response body as dict/list or error string). """ url = f"{base_url.rstrip('/')}{endpoint}" headers = {"Content-Type": "application/json"} if jwt_token: headers["Authorization"] = f"Bearer {jwt_token}" body = None if data is not None: body = json.dumps(data).encode("utf-8") delay = 1.0 for attempt in range(max_retries + 1): req = urllib.request.Request(url, data=body, headers=headers, method=method) try: with urllib.request.urlopen(req, timeout=30) as resp: status = resp.status content = resp.read().decode("utf-8") if content: return status, json.loads(content) return status, {} except urllib.error.HTTPError as e: content = e.read().decode("utf-8") if e.fp else "" # Retry on rate limit if e.code == 429 and attempt < max_retries: time.sleep(delay) delay *= 2 # Exponential backoff continue try: return e.code, json.loads(content) if content else {"error": str(e)} except json.JSONDecodeError: return e.code, {"error": content or str(e)} except urllib.error.URLError as e: return 0, {"error": str(e)} return 429, {"error": "Rate limit exceeded after retries"} def upload_document( base_url: str, client_id: str, filename: str, content: bytes, jwt_token: Optional[str] = None ) -> Tuple[int, Any]: """ Upload a document via multipart/form-data. Args: base_url: Base URL of the API. client_id: Client ID to upload document to. filename: Name of the file being uploaded. content: File content as bytes. jwt_token: Optional JWT token for authorization. Returns: Tuple of (HTTP status code, response body). """ url = f"{base_url.rstrip('/')}/client/{client_id}/document" boundary = f"----WebKitFormBoundary{int(time.time() * 1000)}" # Build multipart form data - content type must be application/octet-stream body_parts = [] body_parts.append(f"--{boundary}".encode()) body_parts.append( f'Content-Disposition: form-data; name="file"; filename="{filename}"'.encode() ) body_parts.append(b"Content-Type: application/octet-stream") body_parts.append(b"") body_parts.append(content) body_parts.append(f"--{boundary}--".encode()) body = b"\r\n".join(body_parts) headers = { "Content-Type": f"multipart/form-data; boundary={boundary}", "Content-Length": str(len(body)) } if jwt_token: headers["Authorization"] = f"Bearer {jwt_token}" req = urllib.request.Request(url, data=body, headers=headers, method="POST") try: with urllib.request.urlopen(req, timeout=60) as resp: status = resp.status resp_content = resp.read().decode("utf-8") if resp_content: return status, json.loads(resp_content) return status, {} except urllib.error.HTTPError as e: err_content = e.read().decode("utf-8") if e.fp else "" try: return e.code, json.loads(err_content) if err_content else {"error": str(e)} except json.JSONDecodeError: return e.code, {"error": err_content or str(e)} except urllib.error.URLError as e: return 0, {"error": str(e)} def create_pdf(unique_text: str) -> bytes: """ Create a minimal valid PDF file with unique content. The PDF contains the unique_text which makes its hash unique. Args: unique_text: Text to embed in the PDF to make it unique. Returns: PDF file content as bytes. """ stream_content = f"""BT /F1 12 Tf 50 700 Td ({unique_text}) Tj ET""" stream_length = len(stream_content) pdf = f"""%PDF-1.4 %\xe2\xe3\xcf\xd3 1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj 2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 >> endobj 3 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Resources << /Font << /F1 << /Type /Font /Subtype /Type1 /BaseFont /Helvetica >> >> >> /Contents 4 0 R >> endobj 4 0 obj << /Length {stream_length} >> stream {stream_content} endstream endobj xref 0 5 0000000000 65535 f 0000000015 00000 n 0000000066 00000 n 0000000125 00000 n 0000000330 00000 n trailer << /Size 5 /Root 1 0 R >> startxref 430 %%EOF""" return pdf.encode("latin-1") def read_csv_file(csv_path: str) -> Tuple[List[str], List[List[str]]]: """ Read and parse the CSV file. Args: csv_path: Path to the CSV file. Returns: Tuple of (headers list, data rows list). """ with open(csv_path, "r", encoding="utf-8-sig") as f: reader = csv.reader(f) headers = next(reader) rows = list(reader) return headers, rows def group_rows_by_filename(rows: List[List[str]]) -> Dict[str, List[List[str]]]: """ Group CSV rows by FILE_NAME (first column). Args: rows: List of CSV data rows. Returns: Dictionary mapping file names to their rows. """ groups = defaultdict(list) for row in rows: if row: # Skip empty rows filename = row[0] if row else "" if filename: groups[filename].append(row) return dict(groups) def extract_single_fields(row: List[str]) -> Dict[str, Any]: """ Extract single-value fields from a CSV row. Args: row: A CSV data row. Returns: Dictionary of single field name -> value. """ fields = {} for col_idx, field_name in SINGLE_FIELD_COLUMNS.items(): if col_idx < len(row): value = convert_field_value(field_name, row[col_idx]) if value is not None: fields[field_name] = value return fields def extract_array_field_item(row: List[str]) -> Dict[str, Any]: """ Extract array field item from a CSV row. Args: row: A CSV data row. Returns: Dictionary of array field name -> value. """ item = {} for col_idx, field_name in ARRAY_FIELD_COLUMNS.items(): if col_idx < len(row): value = convert_field_value(field_name, row[col_idx]) if value is not None: item[field_name] = value return item def create_client(base_url: str, jwt_token: Optional[str] = None) -> Optional[str]: """ Create a demo client via the API. Args: base_url: Base URL of the API. jwt_token: Optional JWT token. Returns: Client ID if successful, None otherwise. """ timestamp = int(time.time()) client_data = { "id": f"demo-data-loader-{timestamp}", "name": f"Demo Data Loader Client {timestamp}" } print_info(f"Creating client: {client_data['name']}") status, response = api_request(base_url, "/client", "POST", client_data, jwt_token) if status == 201: client_id = response.get("id") print_success(f"Client created: {client_id}") return client_id else: print_error(f"Failed to create client: {status} - {response}") return None def upload_documents( base_url: str, client_id: str, filenames: List[str], jwt_token: Optional[str] = None ) -> Dict[str, str]: """ Upload unique PDF documents for each filename. Args: base_url: Base URL of the API. client_id: Client ID to upload to. filenames: List of unique filenames from CSV. jwt_token: Optional JWT token. Returns: Dictionary mapping original filename to document ID. """ filename_to_doc_id = {} for i, filename in enumerate(filenames): # Create unique PDF with timestamp and index unique_text = f"Demo Document: {filename} - Created: {datetime.now().isoformat()} - Index: {i}" pdf_content = create_pdf(unique_text) pdf_filename = f"{filename}.pdf" print_info(f"Uploading: {pdf_filename}") status, response = upload_document( base_url, client_id, pdf_filename, pdf_content, jwt_token ) if status == 200: # Response may contain document info or we need to fetch it doc_id = response.get("id") or response.get("document_id") if doc_id: filename_to_doc_id[filename] = doc_id print_success(f"Uploaded: {pdf_filename} -> {doc_id}") else: print_info(f"Uploaded {pdf_filename} (processing async)") # Will need to retrieve document ID later else: print_error(f"Failed to upload {pdf_filename}: {status} - {response}") # Delay to avoid rate limiting time.sleep(0.5) return filename_to_doc_id def wait_for_documents( base_url: str, client_id: str, expected_count: int, jwt_token: Optional[str] = None, max_attempts: int = 30 ) -> List[Dict]: """ Wait for documents to be processed and return their details. Args: base_url: Base URL of the API. client_id: Client ID to query. expected_count: Number of documents expected. jwt_token: Optional JWT token. max_attempts: Maximum polling attempts. Returns: List of document records with full details including filename. """ print_info(f"Waiting for {expected_count} documents to be processed...") doc_count = 0 doc_ids = [] for attempt in range(max_attempts): status, response = api_request( base_url, f"/client/{client_id}/document", "GET", jwt_token=jwt_token ) if status == 200 and isinstance(response, list): doc_count = len(response) doc_ids = [d.get("id") for d in response if d.get("id")] print_info(f" Attempt {attempt + 1}: {doc_count}/{expected_count} documents") if doc_count >= expected_count: print_success(f"All {doc_count} documents are available") break time.sleep(1) if doc_count == 0: print_info("Timeout: No documents available") return [] # Fetch full details for each document to get filenames print_info("Fetching document details...") full_documents = [] for doc_id in doc_ids: status, doc_detail = api_request( base_url, f"/document/{doc_id}", "GET", jwt_token=jwt_token ) if status == 200: full_documents.append(doc_detail) time.sleep(0.05) # Small delay to avoid rate limiting return full_documents def create_field_extraction( base_url: str, document_id: str, single_fields: Dict[str, Any], array_fields: List[Dict[str, Any]], jwt_token: Optional[str] = None ) -> bool: """ Create a field extraction record for a document. Args: base_url: Base URL of the API. document_id: Document ID to create extraction for. single_fields: Single-value fields dictionary. array_fields: List of array field items. jwt_token: Optional JWT token. Returns: True if successful, False otherwise. """ extraction_data = { "documentId": document_id, "singleFields": single_fields, "arrayFields": array_fields, "createdBy": "demo-data-loader@example.com" } status, response = api_request( base_url, "/field-extractions", "POST", extraction_data, jwt_token ) if status == 201: version = response.get("version", "?") print_success(f"Field extraction created: version={version}") return True else: print_error(f"Failed to create extraction: {status} - {response}") return False def verify_field_extractions( base_url: str, document_ids: List[str], csv_data: Dict[str, Dict], jwt_token: Optional[str] = None ) -> Tuple[int, int]: """ Verify that field extractions match the CSV input data. Args: base_url: Base URL of the API. document_ids: List of document IDs to verify. csv_data: Dictionary mapping filename to expected extraction data. jwt_token: Optional JWT token. Returns: Tuple of (success_count, failure_count). """ success_count = 0 failure_count = 0 for doc_id in document_ids: status, response = api_request( base_url, f"/field-extractions?documentId={doc_id}", "GET", jwt_token=jwt_token ) if status == 200: single_fields = response.get("singleFields", {}) array_fields = response.get("arrayFields", []) filename = single_fields.get("fileName", "") # Basic validation: check that fields exist if single_fields and array_fields: print_success( f"Verified {doc_id}: {len(single_fields)} single fields, " f"{len(array_fields)} array items" ) success_count += 1 else: print_error(f"Incomplete extraction for {doc_id}") failure_count += 1 elif status == 404: print_error(f"No extraction found for {doc_id}") failure_count += 1 else: print_error(f"Failed to verify {doc_id}: {status}") failure_count += 1 return success_count, failure_count def parse_arguments() -> Tuple[str, str, Optional[str]]: """ Parse command line arguments. Supports: - positional: csv_path, base_url, [jwt_token] - named: tokenfile= to read token from file Returns: Tuple of (csv_path, base_url, jwt_token or None). """ if len(sys.argv) < 3: print("Usage: python3 demo_data_loader.py [jwt_token | tokenfile=]") print("") print("Arguments:") print(" csv_path - Path to the CSV file containing demo data") print(" base_url - Base URL of the API (e.g., http://localhost:8080)") print(" jwt_token - Optional JWT token for authentication") print(" tokenfile= - Optional path to file containing JWT token") print("") print("Examples:") print(" python3 demo_data_loader.py SaaS_Demo_Data_Sam.csv http://localhost:8080") print(" python3 demo_data_loader.py SaaS_Demo_Data_Sam.csv http://localhost:8080 tokenfile=./token.txt") sys.exit(1) csv_path = sys.argv[1] base_url = sys.argv[2] jwt_token = None # Check for optional third argument if len(sys.argv) > 3: arg3 = sys.argv[3] if arg3.startswith("tokenfile="): # Read token from file token_path = arg3[len("tokenfile="):] jwt_token = read_token_file(token_path) if jwt_token is None: print(f" [ERROR] Could not read token from: {token_path}") sys.exit(1) else: # Treat as direct token jwt_token = arg3 return csv_path, base_url, jwt_token def main(): """Main entry point for the demo data loader script.""" # Parse command line arguments csv_path, base_url, jwt_token = parse_arguments() print_header("Demo Data Loader") print_info(f"CSV File: {csv_path}") print_info(f"Base URL: {base_url}") if jwt_token: # Show truncated token for security token_preview = jwt_token[:20] + "..." if len(jwt_token) > 20 else jwt_token print_info(f"JWT Token: {token_preview}") else: print_info("JWT Token: not provided") # Step 1: Read and parse CSV print_header("Step 1: Reading CSV File") try: headers, rows = read_csv_file(csv_path) print_success(f"Read {len(rows)} data rows with {len(headers)} columns") except Exception as e: print_error(f"Failed to read CSV: {e}") sys.exit(1) # Step 2: Group rows by filename print_header("Step 2: Analyzing Data") file_groups = group_rows_by_filename(rows) unique_files = list(file_groups.keys()) print_success(f"Found {len(unique_files)} unique documents to import") for filename in unique_files[:5]: # Show first 5 row_count = len(file_groups[filename]) print_info(f" - {filename}: {row_count} array item(s)") if len(unique_files) > 5: print_info(f" ... and {len(unique_files) - 5} more") # Step 3: Create demo client print_header("Step 3: Creating Demo Client") client_id = create_client(base_url, jwt_token) if not client_id: print_error("Cannot proceed without a client") sys.exit(1) # Step 4: Upload documents print_header("Step 4: Uploading Documents") filename_to_doc_id = upload_documents(base_url, client_id, unique_files, jwt_token) # Step 5: Wait for documents to be processed print_header("Step 5: Waiting for Document Processing") documents = wait_for_documents(base_url, client_id, len(unique_files), jwt_token) if not documents: print_error("No documents available after upload") sys.exit(1) # Build mapping from filename to document ID for doc in documents: doc_filename = doc.get("filename", "") doc_id = doc.get("id", "") if not doc_id or not doc_filename: continue # Remove .pdf extension for matching base_name = doc_filename if base_name.lower().endswith(".pdf"): base_name = base_name[:-4] if base_name in file_groups: filename_to_doc_id[base_name] = doc_id print_success(f"Mapped {len(filename_to_doc_id)} filenames to document IDs") # Debug: show any unmatched files unmatched_csv = [f for f in file_groups.keys() if f not in filename_to_doc_id] if unmatched_csv: print_info(f"Unmatched CSV files ({len(unmatched_csv)}):") for f in unmatched_csv[:5]: print_info(f" - {f}") # Step 6: Create field extractions print_header("Step 6: Creating Field Extractions") extraction_success = 0 extraction_failed = 0 for filename, doc_rows in file_groups.items(): doc_id = filename_to_doc_id.get(filename) if not doc_id: print_error(f"No document ID for {filename}") extraction_failed += 1 continue print_info(f"Processing: {filename}") # Extract single fields from first row (they should be same for all rows) single_fields = extract_single_fields(doc_rows[0]) # Extract array fields from all rows array_fields = [] for row in doc_rows: array_item = extract_array_field_item(row) if array_item: # Only add non-empty items array_fields.append(array_item) # Create field extraction if create_field_extraction(base_url, doc_id, single_fields, array_fields, jwt_token): extraction_success += 1 else: extraction_failed += 1 # Delay to avoid rate limiting time.sleep(0.5) print_success(f"Created {extraction_success} extractions, {extraction_failed} failed") # Step 7: Verify extractions print_header("Step 7: Verifying Field Extractions") doc_ids_to_verify = list(filename_to_doc_id.values()) verified, failed = verify_field_extractions(base_url, doc_ids_to_verify, {}, jwt_token) # Final summary print_header("Summary") print_info(f"Client ID: {client_id}") print_info(f"Documents uploaded: {len(documents)}") print_info(f"Field extractions created: {extraction_success}") print_info(f"Verifications passed: {verified}") print_info(f"Verifications failed: {failed}") if extraction_failed == 0 and failed == 0: print_success("All operations completed successfully!") sys.exit(0) else: print_error(f"Some operations failed: {extraction_failed} extractions, {failed} verifications") sys.exit(1) if __name__ == "__main__": main()