initial commit

This commit is contained in:
2026-05-11 12:36:20 +05:30
commit 384cbe8019
15377 changed files with 2360544 additions and 0 deletions

View File

@@ -0,0 +1,368 @@
"""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