{"kind":"AgentDefinition","metadata":{"namespace":"community","name":"dataverse-python-error-handling","version":"0.1.0"},"spec":{"agents_md":"---\napplyTo: '**'\n---\n\n# Dataverse SDK for Python — Error Handling \u0026 Troubleshooting Guide\n\nBased on official Microsoft documentation for Azure SDK error handling patterns and Dataverse SDK specifics.\n\n## 1. DataverseError Class Overview\n\nThe Dataverse SDK for Python provides a structured exception hierarchy for robust error handling.\n\n### DataverseError Constructor\n\n```python\nfrom PowerPlatform.Dataverse.core.errors import DataverseError\n\nDataverseError(\n    message: str,                          # Human-readable error message\n    code: str,                             # Error category (e.g., \"validation_error\", \"http_error\")\n    subcode: str | None = None,            # Optional specific error identifier\n    status_code: int | None = None,        # HTTP status code (if applicable)\n    details: Dict[str, Any] | None = None, # Additional diagnostic information\n    source: str | None = None,             # Error source: \"client\" or \"server\"\n    is_transient: bool = False             # Whether error may succeed on retry\n)\n```\n\n### Key Properties\n\n```python\ntry:\n    client.get(\"account\", record_id=\"invalid-id\")\nexcept DataverseError as e:\n    print(f\"Message: {e.message}\")           # Human-readable message\n    print(f\"Code: {e.code}\")                 # Error category\n    print(f\"Subcode: {e.subcode}\")           # Specific error type\n    print(f\"Status Code: {e.status_code}\")   # HTTP status (401, 403, 429, etc.)\n    print(f\"Source: {e.source}\")             # \"client\" or \"server\"\n    print(f\"Is Transient: {e.is_transient}\") # Can retry?\n    print(f\"Details: {e.details}\")           # Additional context\n    \n    # Convert to dictionary for logging\n    error_dict = e.to_dict()\n```\n\n---\n\n## 2. Common Error Scenarios\n\n### Authentication Errors (401)\n\n**Cause**: Invalid credentials, expired tokens, or misconfigured settings.\n\n```python\nfrom PowerPlatform.Dataverse.client import DataverseClient\nfrom PowerPlatform.Dataverse.core.errors import DataverseError\nfrom azure.identity import InteractiveBrowserCredential\n\ntry:\n    # Bad credentials or expired token\n    credential = InteractiveBrowserCredential()\n    client = DataverseClient(\n        base_url=\"https://invalid-org.crm.dynamics.com\",\n        credential=credential\n    )\n    records = client.get(\"account\")\nexcept DataverseError as e:\n    if e.status_code == 401:\n        print(\"Authentication failed. Check credentials and token expiration.\")\n        print(f\"Details: {e.message}\")\n        # Don't retry - fix credentials first\n    else:\n        raise\n```\n\n### Authorization Errors (403)\n\n**Cause**: User lacks permissions for the requested operation.\n\n```python\ntry:\n    # User doesn't have permission to read contacts\n    records = client.get(\"contact\")\nexcept DataverseError as e:\n    if e.status_code == 403:\n        print(\"Access denied. User lacks required permissions.\")\n        print(f\"Request ID for support: {e.details.get('request_id')}\")\n        # Escalate to administrator\n    else:\n        raise\n```\n\n### Resource Not Found (404)\n\n**Cause**: Record, table, or resource doesn't exist.\n\n```python\ntry:\n    # Record doesn't exist\n    record = client.get(\"account\", record_id=\"00000000-0000-0000-0000-000000000000\")\nexcept DataverseError as e:\n    if e.status_code == 404:\n        print(\"Resource not found. Using default data.\")\n        record = {\"name\": \"Unknown\", \"id\": None}\n    else:\n        raise\n```\n\n### Rate Limiting (429)\n\n**Cause**: Too many requests exceeding service protection limits.\n\n**Note**: The SDK has minimal built-in retry support. Handle transient consistency issues manually.\n\n```python\nimport time\n\ndef create_with_retry(client, table_name, payload, max_retries=3):\n    \"\"\"Create record with retry logic for rate limiting.\"\"\"\n    for attempt in range(max_retries):\n        try:\n            result = client.create(table_name, payload)\n            return result\n        except DataverseError as e:\n            if e.status_code == 429 and e.is_transient:\n                wait_time = 2 ** attempt  # Exponential backoff\n                print(f\"Rate limited. Retrying in {wait_time}s...\")\n                time.sleep(wait_time)\n            else:\n                raise\n    \n    raise Exception(f\"Failed after {max_retries} retries\")\n```\n\n### Server Errors (500, 502, 503, 504)\n\n**Cause**: Temporary service issues or infrastructure problems.\n\n```python\ntry:\n    result = client.create(\"account\", {\"name\": \"Acme\"})\nexcept DataverseError as e:\n    if 500 \u003c= e.status_code \u003c 600:\n        print(f\"Server error ({e.status_code}). Service may be temporarily unavailable.\")\n        # Implement retry logic with exponential backoff\n    else:\n        raise\n```\n\n### Validation Errors (400)\n\n**Cause**: Invalid request format, missing required fields, or business rule violations.\n\n```python\ntry:\n    # Missing required field or invalid data\n    client.create(\"account\", {\"telephone1\": \"not-a-phone-number\"})\nexcept DataverseError as e:\n    if e.status_code == 400:\n        print(f\"Validation error: {e.message}\")\n        if e.details:\n            print(f\"Details: {e.details}\")\n        # Log validation issues for debugging\n    else:\n        raise\n```\n\n---\n\n## 3. Error Handling Best Practices\n\n### Use Specific Exception Handling\n\nAlways catch specific exceptions before general ones:\n\n```python\nfrom PowerPlatform.Dataverse.core.errors import DataverseError\nfrom azure.core.exceptions import AzureError\n\ntry:\n    records = client.get(\"account\", filter=\"statecode eq 0\", top=100)\nexcept DataverseError as e:\n    # Handle Dataverse-specific errors\n    if e.status_code == 401:\n        print(\"Re-authenticate required\")\n    elif e.status_code == 404:\n        print(\"Resource not found\")\n    elif e.is_transient:\n        print(\"Transient error - may retry\")\n    else:\n        print(f\"Operation failed: {e.message}\")\nexcept AzureError as e:\n    # Handle Azure SDK errors (network, auth, etc.)\n    print(f\"Azure error: {e}\")\nexcept Exception as e:\n    # Catch-all for unexpected errors\n    print(f\"Unexpected error: {e}\")\n```\n\n### Implement Smart Retry Logic\n\n**Don't retry on**:\n- 401 Unauthorized (authentication failures)\n- 403 Forbidden (authorization failures)\n- 400 Bad Request (client errors)\n- 404 Not Found (unless resource should eventually appear)\n\n**Consider retrying on**:\n- 408 Request Timeout\n- 429 Too Many Requests (with exponential backoff)\n- 500 Internal Server Error\n- 502 Bad Gateway\n- 503 Service Unavailable\n- 504 Gateway Timeout\n\n```python\ndef should_retry(error: DataverseError) -\u003e bool:\n    \"\"\"Determine if operation should be retried.\"\"\"\n    if not error.is_transient:\n        return False\n    \n    retryable_codes = {408, 429, 500, 502, 503, 504}\n    return error.status_code in retryable_codes\n\ndef call_with_exponential_backoff(func, *args, max_attempts=3, **kwargs):\n    \"\"\"Call function with exponential backoff retry.\"\"\"\n    for attempt in range(max_attempts):\n        try:\n            return func(*args, **kwargs)\n        except DataverseError as e:\n            if should_retry(e) and attempt \u003c max_attempts - 1:\n                wait_time = 2 ** attempt  # 1s, 2s, 4s...\n                print(f\"Attempt {attempt + 1} failed. Retrying in {wait_time}s...\")\n                time.sleep(wait_time)\n            else:\n                raise\n```\n\n### Extract Meaningful Error Information\n\n```python\nimport json\nfrom datetime import datetime\n\ndef log_error_for_support(error: DataverseError):\n    \"\"\"Log error with diagnostic information.\"\"\"\n    error_info = {\n        \"timestamp\": datetime.utcnow().isoformat(),\n        \"error_type\": type(error).__name__,\n        \"message\": error.message,\n        \"code\": error.code,\n        \"subcode\": error.subcode,\n        \"status_code\": error.status_code,\n        \"source\": error.source,\n        \"is_transient\": error.is_transient,\n        \"details\": error.details\n    }\n    \n    print(json.dumps(error_info, indent=2))\n    \n    # Save to log file or send to monitoring service\n    return error_info\n```\n\n### Handle Bulk Operations Gracefully\n\n```python\ndef bulk_create_with_error_tracking(client, table_name, payloads):\n    \"\"\"Create multiple records, tracking which succeed/fail.\"\"\"\n    results = {\n        \"succeeded\": [],\n        \"failed\": []\n    }\n    \n    for idx, payload in enumerate(payloads):\n        try:\n            record_ids = client.create(table_name, payload)\n            results[\"succeeded\"].append({\n                \"payload\": payload,\n                \"ids\": record_ids\n            })\n        except DataverseError as e:\n            results[\"failed\"].append({\n                \"index\": idx,\n                \"payload\": payload,\n                \"error\": {\n                    \"message\": e.message,\n                    \"code\": e.code,\n                    \"status\": e.status_code\n                }\n            })\n    \n    return results\n```\n\n---\n\n## 4. Enable Diagnostic Logging\n\n### Configure Logging\n\n```python\nimport logging\nimport sys\n\n# Set up root logger\nlogging.basicConfig(\n    level=logging.DEBUG,\n    format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',\n    handlers=[\n        logging.FileHandler('dataverse_sdk.log'),\n        logging.StreamHandler(sys.stdout)\n    ]\n)\n\n# Configure specific loggers\nlogging.getLogger('azure').setLevel(logging.DEBUG)\nlogging.getLogger('PowerPlatform').setLevel(logging.DEBUG)\n\n# HTTP logging (careful with sensitive data)\nlogging.getLogger('azure.core.pipeline.policies.http_logging_policy').setLevel(logging.DEBUG)\n```\n\n### Enable SDK-Level Logging\n\n```python\nfrom PowerPlatform.Dataverse.client import DataverseClient\nfrom PowerPlatform.Dataverse.core.config import DataverseConfig\nfrom azure.identity import InteractiveBrowserCredential\n\ncfg = DataverseConfig()\ncfg.logging_enable = True  # Enable detailed logging\n\nclient = DataverseClient(\n    base_url=\"https://myorg.crm.dynamics.com\",\n    credential=InteractiveBrowserCredential(),\n    config=cfg\n)\n\n# Now SDK will log detailed HTTP requests/responses\nrecords = client.get(\"account\", top=10)\n```\n\n### Parse Error Responses\n\n```python\nimport json\n\ntry:\n    client.create(\"account\", invalid_payload)\nexcept DataverseError as e:\n    # Extract structured error details\n    if e.details and isinstance(e.details, dict):\n        error_code = e.details.get('error', {}).get('code')\n        error_message = e.details.get('error', {}).get('message')\n        \n        print(f\"Error Code: {error_code}\")\n        print(f\"Error Message: {error_message}\")\n        \n        # Some errors include nested details\n        if 'error' in e.details and 'details' in e.details['error']:\n            for detail in e.details['error']['details']:\n                print(f\"  - {detail.get('code')}: {detail.get('message')}\")\n```\n\n---\n\n## 5. Dataverse-Specific Error Handling\n\n### Handle OData Query Errors\n\n```python\ntry:\n    # Invalid OData filter\n    records = client.get(\n        \"account\",\n        filter=\"invalid_column eq 0\"\n    )\nexcept DataverseError as e:\n    if \"invalid column\" in e.message.lower():\n        print(\"Check OData column names and syntax\")\n    else:\n        print(f\"Query error: {e.message}\")\n```\n\n### Handle File Upload Errors\n\n```python\ntry:\n    client.upload_file(\n        table_name=\"account\",\n        record_id=record_id,\n        column_name=\"document_column\",\n        file_path=\"large_file.pdf\"\n    )\nexcept DataverseError as e:\n    if e.status_code == 413:\n        print(\"File too large. Use chunked upload mode.\")\n    elif e.status_code == 400:\n        print(\"Invalid column or file format.\")\n    else:\n        raise\n```\n\n### Handle Table Metadata Operations\n\n```python\ntry:\n    # Create custom table\n    table_def = {\n        \"SchemaName\": \"new_CustomTable\",\n        \"DisplayName\": \"Custom Table\"\n    }\n    client.create(\"EntityMetadata\", table_def)\nexcept DataverseError as e:\n    if \"already exists\" in e.message:\n        print(\"Table already exists\")\n    elif \"permission\" in e.message.lower():\n        print(\"Insufficient permissions to create tables\")\n    else:\n        raise\n```\n\n---\n\n## 6. Monitoring and Alerting\n\n### Wrap Client Calls with Monitoring\n\n```python\nfrom functools import wraps\nimport time\n\ndef monitor_operation(operation_name):\n    \"\"\"Decorator to monitor SDK operations.\"\"\"\n    def decorator(func):\n        @wraps(func)\n        def wrapper(*args, **kwargs):\n            start_time = time.time()\n            try:\n                result = func(*args, **kwargs)\n                duration = time.time() - start_time\n                print(f\"✓ {operation_name} completed in {duration:.2f}s\")\n                return result\n            except DataverseError as e:\n                duration = time.time() - start_time\n                print(f\"✗ {operation_name} failed after {duration:.2f}s\")\n                print(f\"  Error: {e.code} ({e.status_code}): {e.message}\")\n                raise\n        return wrapper\n    return decorator\n\n@monitor_operation(\"Fetch Accounts\")\ndef get_accounts(client):\n    return client.get(\"account\", top=100)\n\n# Usage\ntry:\n    accounts = get_accounts(client)\nexcept DataverseError:\n    print(\"Operation failed - check logs for details\")\n```\n\n---\n\n## 7. Common Troubleshooting Checklist\n\n| Issue | Diagnosis | Solution |\n|-------|-----------|----------|\n| 401 Unauthorized | Expired token or bad credentials | Re-authenticate with valid credentials |\n| 403 Forbidden | User lacks permissions | Request access from administrator |\n| 404 Not Found | Record/table doesn't exist | Verify schema name and record ID |\n| 429 Rate Limited | Too many requests | Implement exponential backoff retry |\n| 500+ Server Error | Service issue | Retry with exponential backoff; check status page |\n| 400 Bad Request | Invalid request format | Check OData syntax, field names, required fields |\n| Network timeout | Connection issues | Check network, increase timeout in DataverseConfig |\n| InvalidOperationException | Plugin/workflow error | Check plugin logs in Dataverse |\n\n---\n\n## 8. Logging Best Practices\n\n```python\nimport logging\nimport json\nfrom datetime import datetime\n\nclass DataverseErrorHandler:\n    \"\"\"Centralized error handling and logging.\"\"\"\n    \n    def __init__(self, log_file=\"dataverse_errors.log\"):\n        self.logger = logging.getLogger(\"DataverseSDK\")\n        handler = logging.FileHandler(log_file)\n        formatter = logging.Formatter(\n            '%(asctime)s - %(levelname)s - %(message)s'\n        )\n        handler.setFormatter(formatter)\n        self.logger.addHandler(handler)\n        self.logger.setLevel(logging.ERROR)\n    \n    def log_error(self, error: DataverseError, context: str = \"\"):\n        \"\"\"Log error with context for debugging.\"\"\"\n        error_record = {\n            \"timestamp\": datetime.utcnow().isoformat(),\n            \"context\": context,\n            \"error\": error.to_dict()\n        }\n        \n        self.logger.error(json.dumps(error_record, indent=2))\n    \n    def is_retryable(self, error: DataverseError) -\u003e bool:\n        \"\"\"Check if error should be retried.\"\"\"\n        return error.is_transient and error.status_code in {408, 429, 500, 502, 503, 504}\n\n# Usage\nerror_handler = DataverseErrorHandler()\n\ntry:\n    client.create(\"account\", payload)\nexcept DataverseError as e:\n    error_handler.log_error(e, \"create_account_batch_1\")\n    if error_handler.is_retryable(e):\n        print(\"Will retry this operation\")\n    else:\n        print(\"Operation failed permanently\")\n```\n\n---\n\n## 9. See Also\n\n- [DataverseError API Reference](https://learn.microsoft.com/en-us/python/api/powerplatform-dataverse-client/powerplatform.dataverse.core.errors.dataverseerror)\n- [Azure SDK Error Handling](https://learn.microsoft.com/en-us/azure/developer/python/sdk/fundamentals/errors)\n- [Dataverse SDK Getting Started](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/sdk-python/get-started)\n- [Service Protection API Limits](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/optimize-performance-create-update)\n","description":"Based on official Microsoft documentation for Azure SDK error handling patterns and Dataverse SDK specifics.","import":{"commit_sha":"541b7819d8c3545c6df122491af4fa1eae415779","imported_at":"2026-05-18T20:05:35Z","license_text":"MIT License\n\nCopyright GitHub, Inc.\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.","owner":"github","repo":"github/awesome-copilot","source_url":"https://github.com/github/awesome-copilot/blob/541b7819d8c3545c6df122491af4fa1eae415779/instructions/dataverse-python-error-handling.instructions.md"},"manifest":{}},"content_hash":[179,74,85,159,57,50,162,20,164,169,176,193,201,189,98,209,142,137,122,210,70,205,216,186,214,242,233,17,105,103,202,124],"trust_level":"unsigned","yanked":false}
