Files
Analytical_engine_backend/venv/Lib/site-packages/langsmith/sandbox/_helpers.py
2026-05-11 12:36:20 +05:30

369 lines
13 KiB
Python

"""Shared helper functions for error handling.
These functions are used by both sync and async clients to parse error responses
and raise appropriate exceptions. They contain no I/O operations.
"""
from __future__ import annotations
from typing import Any, Optional
import httpx
from langsmith.sandbox._exceptions import (
QuotaExceededError,
ResourceAlreadyExistsError,
ResourceCreationError,
ResourceNotFoundError,
ResourceTimeoutError,
SandboxAPIError,
SandboxAuthenticationError,
SandboxClientError,
SandboxConnectionError,
SandboxNotReadyError,
SandboxOperationError,
ValidationError,
)
# =============================================================================
# Error Response Parsing
# =============================================================================
def parse_error_response(error: httpx.HTTPStatusError) -> dict[str, Any]:
"""Parse standardized error response.
Expected format: {"detail": {"error": "...", "message": "..."}}
Returns a dict with:
- error_type: The error type (e.g., "ImagePull", "CrashLoop")
- message: Human-readable error message
"""
try:
data = error.response.json()
detail = data.get("detail")
# Standardized format: {"detail": {"error": "...", "message": "..."}}
if isinstance(detail, dict):
return {
"error_type": detail.get("error"),
"message": detail.get("message", str(error)),
}
# Pydantic validation error format: {"detail": [{"loc": [...], "msg": "..."}]}
if isinstance(detail, list) and detail:
messages = [d.get("msg", str(d)) for d in detail if isinstance(d, dict)]
return {
"error_type": None,
"message": "; ".join(messages) if messages else str(error),
}
# Fallback for plain string detail
return {"error_type": None, "message": detail or str(error)}
except Exception:
return {"error_type": None, "message": str(error)}
def parse_error_response_simple(error: httpx.HTTPStatusError) -> dict[str, Any]:
"""Parse error response (simplified version for sandbox operations).
Returns a dict with:
- error_type: The error type
- message: Human-readable error message
"""
try:
data = error.response.json()
detail = data.get("detail")
if isinstance(detail, dict):
return {
"error_type": detail.get("error"),
"message": detail.get("message", str(error)),
}
return {"error_type": None, "message": detail or str(error)}
except Exception:
return {"error_type": None, "message": str(error)}
def parse_validation_error(error: httpx.HTTPStatusError) -> list[dict]:
"""Parse Pydantic validation error response.
Returns a list of validation error details, each containing:
- loc: Location of the error (e.g., ["body", "resources", "cpu"])
- msg: Human-readable error message
- type: Error type (e.g., "value_error")
"""
try:
data = error.response.json()
detail = data.get("detail", [])
if isinstance(detail, list):
return detail
return []
except Exception:
return []
def extract_quota_type(message: str) -> Optional[str]:
"""Extract quota type from error message.
Returns one of: "sandbox_count", "cpu", "memory", "volume_count",
"storage", or None.
"""
message_lower = message.lower()
# Check for sandbox count quota
if "sandbox" in message_lower and (
"count" in message_lower or "limit" in message_lower
):
return "sandbox_count"
elif "cpu" in message_lower:
return "cpu"
elif "memory" in message_lower:
return "memory"
# Check for volume count quota
elif "volume" in message_lower and (
"count" in message_lower or "limit" in message_lower
):
return "volume_count"
elif "storage" in message_lower:
return "storage"
return None
# =============================================================================
# Client Error Handlers
# =============================================================================
def raise_creation_error(
data: dict[str, Any],
error: httpx.HTTPStatusError,
resource_type: str = "sandbox",
) -> None:
"""Raise ResourceCreationError with the error_type from the API response.
The error_type indicates the specific failure reason:
- ImagePull: Image pull failed
- CrashLoop: Container crashed during startup
- SandboxConfig: Configuration error
- Unschedulable: Cannot be scheduled
"""
raise ResourceCreationError(
data.get("message", f"{resource_type.title()} creation failed"),
resource_type=resource_type,
error_type=data.get("error_type"),
) from error
def handle_sandbox_creation_error(error: httpx.HTTPStatusError) -> None:
"""Handle HTTP errors specific to sandbox creation.
Maps API error responses to specific exception types:
- 408: ResourceTimeoutError (sandbox didn't become ready in time)
- 422: ValidationError (bad input) or ResourceCreationError (runtime)
- 429: QuotaExceededError (org limits exceeded)
- 503: ResourceCreationError (no resources available)
- Other: Falls through to generic error handling
"""
status = error.response.status_code
data = parse_error_response(error)
if status == 408:
# Timeout - include the message which contains last known status
raise ResourceTimeoutError(data["message"], resource_type="sandbox") from error
elif status == 422:
# Check if this is a Pydantic validation error (bad input) vs creation error
details = parse_validation_error(error)
if details and any(d.get("type") == "value_error" for d in details):
# Pydantic validation error (bad input - exceeds server limits)
field = details[0].get("loc", [None])[-1] if details else None
raise ValidationError(
message=data["message"],
field=field,
details=details,
) from error
else:
# Sandbox creation failed (runtime error like image pull failure)
raise_creation_error(data, error)
elif status == 429:
# Organization quota exceeded
quota_type = extract_quota_type(data["message"])
raise QuotaExceededError(
message=data["message"],
quota_type=quota_type,
) from error
elif status == 503:
# Service Unavailable - scheduling failed
raise ResourceCreationError(
data["message"],
resource_type="sandbox",
error_type=data.get("error_type") or "Unschedulable",
) from error
else:
# Fall through to generic handling
handle_client_http_error(error)
def handle_volume_creation_error(error: httpx.HTTPStatusError) -> None:
"""Handle HTTP errors specific to volume creation.
Maps API error responses to specific exception types:
- 503: ResourceCreationError (provisioning failed)
- 504: ResourceTimeoutError (volume didn't become ready in time)
- Other: Falls through to generic error handling
"""
status = error.response.status_code
data = parse_error_response(error)
if status == 503:
# Provisioning failed (invalid storage class, quota exceeded)
raise ResourceCreationError(
data["message"],
resource_type="volume",
error_type="VolumeProvisioning",
) from error
elif status == 504:
# Timeout - volume didn't become ready in time
raise ResourceTimeoutError(data["message"], resource_type="volume") from error
else:
# Fall through to generic handling
handle_client_http_error(error)
def handle_pool_error(error: httpx.HTTPStatusError) -> None:
"""Handle HTTP errors specific to pool creation/update.
Maps API error responses to specific exception types:
- 400: ResourceNotFoundError or ValidationError (template has volumes)
- 409: ResourceAlreadyExistsError
- 429: QuotaExceededError (org limits exceeded)
- 504: ResourceTimeoutError (timeout waiting for ready replicas)
- Other: Falls through to generic error handling
"""
status = error.response.status_code
data = parse_error_response(error)
error_type = data.get("error_type")
if status == 400:
# Check the error type to determine the specific exception
if error_type == "TemplateNotFound":
raise ResourceNotFoundError(
data["message"], resource_type="template"
) from error
elif error_type == "ValidationError":
# Template has volumes attached
raise ValidationError(data["message"], error_type=error_type) from error
else:
# Generic bad request
handle_client_http_error(error)
elif status == 409:
# Pool already exists
raise ResourceAlreadyExistsError(
data["message"], resource_type="pool"
) from error
elif status == 429:
# Organization quota exceeded
quota_type = extract_quota_type(data["message"])
raise QuotaExceededError(
message=data["message"],
quota_type=quota_type,
) from error
elif status == 504:
# Timeout waiting for pool to be ready
raise ResourceTimeoutError(data["message"], resource_type="pool") from error
else:
# Fall through to generic handling
handle_client_http_error(error)
def handle_client_http_error(error: httpx.HTTPStatusError) -> None:
"""Handle HTTP errors and raise appropriate exceptions (for client operations)."""
data = parse_error_response(error)
message = data["message"]
error_type = data.get("error_type")
status = error.response.status_code
if status in (401, 403):
raise SandboxAuthenticationError(message) from error
if status == 404:
raise ResourceNotFoundError(message) from error
# Handle validation errors (invalid resource values, formats, etc.)
if status == 422:
details = parse_validation_error(error)
field = details[0].get("loc", [None])[-1] if details else None
raise ValidationError(
message=message,
field=field,
details=details,
) from error
# Handle quota exceeded errors (org limits)
if status == 429:
quota_type = extract_quota_type(message)
raise QuotaExceededError(
message=message,
quota_type=quota_type,
) from error
if status == 502 and error_type == "ConnectionError":
raise SandboxConnectionError(message) from error
if status == 500:
raise SandboxAPIError(message) from error
raise SandboxClientError(message) from error
# =============================================================================
# Sandbox Operation Error Handlers
# =============================================================================
def handle_sandbox_http_error(error: httpx.HTTPStatusError) -> None:
"""Handle HTTP errors for sandbox operations (run, read, write).
Maps API error types to specific exceptions:
- WriteError -> SandboxOperationError (operation="write")
- ReadError -> SandboxOperationError (operation="read")
- CommandError -> SandboxOperationError (operation="command")
- ConnectionError (502) -> SandboxConnectionError
- FileNotFound / 404 -> ResourceNotFoundError (resource_type="file")
- NotReady (400) -> SandboxNotReadyError
- 403 -> SandboxOperationError (permission denied)
"""
data = parse_error_response_simple(error)
message = data["message"]
error_type = data.get("error_type")
status = error.response.status_code
# Operation-specific errors (from sandbox runtime)
if error_type == "WriteError":
raise SandboxOperationError(
message, operation="write", error_type=error_type
) from error
if error_type == "ReadError":
raise SandboxOperationError(
message, operation="read", error_type=error_type
) from error
if error_type == "CommandError":
raise SandboxOperationError(
message, operation="command", error_type=error_type
) from error
# Permission denied
if status == 403:
raise SandboxOperationError(
message, operation=None, error_type="PermissionDenied"
) from error
# Connection to sandbox failed
if status == 502 and error_type == "ConnectionError":
raise SandboxConnectionError(message) from error
# Not ready / not found
if status == 400 and error_type == "NotReady":
raise SandboxNotReadyError(message) from error
if status == 404 or error_type == "FileNotFound":
raise ResourceNotFoundError(message, resource_type="file") from error
raise SandboxClientError(message) from error