""" UCXL Standard Response and Error Codes Library This module provides standardized UCXL error and response codes for consistent cross-service communication and client handling. Based on UCXL-ERROR-CODES.md and UCXL-RESPONSE-CODES.md v1.0 """ import json import uuid from datetime import datetime, timezone from enum import Enum from typing import Any, Dict, Optional from dataclasses import dataclass, field, asdict class UCXLErrorCode(Enum): """Standard UCXL Error Codes following format: UCXL--""" # 400 - Client Errors INVALID_ADDRESS = "UCXL-400-INVALID_ADDRESS" MISSING_FIELD = "UCXL-400-MISSING_FIELD" INVALID_FORMAT = "UCXL-400-INVALID_FORMAT" # 401 - Authentication UNAUTHORIZED = "UCXL-401-UNAUTHORIZED" # 403 - Authorization FORBIDDEN = "UCXL-403-FORBIDDEN" # 404 - Not Found NOT_FOUND = "UCXL-404-NOT_FOUND" # 409 - Conflict CONFLICT = "UCXL-409-CONFLICT" # 422 - Unprocessable Entity UNPROCESSABLE_ENTITY = "UCXL-422-UNPROCESSABLE_ENTITY" # 429 - Rate Limiting RATE_LIMIT = "UCXL-429-RATE_LIMIT" # 500 - Server Errors INTERNAL_ERROR = "UCXL-500-INTERNAL_ERROR" # 503 - Service Unavailable SERVICE_UNAVAILABLE = "UCXL-503-SERVICE_UNAVAILABLE" # 504 - Gateway Timeout GATEWAY_TIMEOUT = "UCXL-504-GATEWAY_TIMEOUT" def http_status(self) -> int: """Get the HTTP status code for this error code""" mapping = { self.INVALID_ADDRESS: 400, self.MISSING_FIELD: 400, self.INVALID_FORMAT: 400, self.UNAUTHORIZED: 401, self.FORBIDDEN: 403, self.NOT_FOUND: 404, self.CONFLICT: 409, self.UNPROCESSABLE_ENTITY: 422, self.RATE_LIMIT: 429, self.INTERNAL_ERROR: 500, self.SERVICE_UNAVAILABLE: 503, self.GATEWAY_TIMEOUT: 504, } return mapping.get(self, 500) def default_message(self) -> str: """Get the default error message for this code""" messages = { self.INVALID_ADDRESS: "Invalid UCXL address format", self.MISSING_FIELD: "Required field is missing", self.INVALID_FORMAT: "Input does not match the expected format", self.UNAUTHORIZED: "Authentication credentials missing or invalid", self.FORBIDDEN: "Insufficient permissions for the action", self.NOT_FOUND: "Requested resource not found", self.CONFLICT: "Conflict with current state", self.UNPROCESSABLE_ENTITY: "Semantic validation failed", self.RATE_LIMIT: "Too many requests; rate limiting in effect", self.INTERNAL_ERROR: "Internal server error", self.SERVICE_UNAVAILABLE: "Service is currently unavailable", self.GATEWAY_TIMEOUT: "Downstream gateway timed out", } return messages.get(self, "Unknown error") def is_client_error(self) -> bool: """Check if this is a client error (4xx)""" return 400 <= self.http_status() < 500 def is_server_error(self) -> bool: """Check if this is a server error (5xx)""" return 500 <= self.http_status() < 600 def should_retry(self) -> bool: """Check if this error should trigger a retry""" return self in { self.RATE_LIMIT, self.INTERNAL_ERROR, self.SERVICE_UNAVAILABLE, self.GATEWAY_TIMEOUT, } class UCXLResponseCode(Enum): """Standard UCXL Response Codes following format: UCXL--""" # 200 - Success OK = "UCXL-200-OK" # 201 - Created CREATED = "UCXL-201-CREATED" # 202 - Accepted (Async) ACCEPTED = "UCXL-202-ACCEPTED" # 204 - No Content NO_CONTENT = "UCXL-204-NO_CONTENT" # 206 - Partial Content PARTIAL_CONTENT = "UCXL-206-PARTIAL_CONTENT" # 304 - Not Modified (Caching) NOT_MODIFIED = "UCXL-304-NOT_MODIFIED" def http_status(self) -> int: """Get the HTTP status code for this response code""" mapping = { self.OK: 200, self.CREATED: 201, self.ACCEPTED: 202, self.NO_CONTENT: 204, self.PARTIAL_CONTENT: 206, self.NOT_MODIFIED: 304, } return mapping.get(self, 200) def default_message(self) -> str: """Get the default success message for this code""" messages = { self.OK: "Request completed successfully", self.CREATED: "Resource created successfully", self.ACCEPTED: "Request accepted for processing", self.NO_CONTENT: "Request completed with no content to return", self.PARTIAL_CONTENT: "Partial results returned", self.NOT_MODIFIED: "Resource not modified since last fetch", } return messages.get(self, "Success") def is_async(self) -> bool: """Check if this indicates an asynchronous operation""" return self == self.ACCEPTED def is_partial(self) -> bool: """Check if this indicates partial/incomplete results""" return self in {self.PARTIAL_CONTENT, self.ACCEPTED} @dataclass class UCXLError: """UCXL Error Details""" code: UCXLErrorCode message: str source: str path: str request_id: str timestamp: datetime details: Optional[Dict[str, Any]] = None cause: Optional[str] = None def __post_init__(self): if isinstance(self.code, str): self.code = UCXLErrorCode(self.code) @dataclass class UCXLErrorResponse: """UCXL Standard Error Response Payload""" error: UCXLError def to_dict(self) -> Dict[str, Any]: """Convert to dictionary""" result = asdict(self) result['error']['code'] = self.error.code.value result['error']['timestamp'] = self.error.timestamp.isoformat() return result def to_json(self) -> str: """Convert to JSON string""" return json.dumps(self.to_dict()) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'UCXLErrorResponse': """Create from dictionary""" error_data = data['error'] error_data['timestamp'] = datetime.fromisoformat(error_data['timestamp'].replace('Z', '+00:00')) return cls(error=UCXLError(**error_data)) @classmethod def from_json(cls, json_str: str) -> 'UCXLErrorResponse': """Create from JSON string""" return cls.from_dict(json.loads(json_str)) @dataclass class UCXLResponse: """UCXL Response Details""" code: UCXLResponseCode message: str request_id: str timestamp: datetime data: Optional[Any] = None details: Optional[Dict[str, Any]] = None def __post_init__(self): if isinstance(self.code, str): self.code = UCXLResponseCode(self.code) @dataclass class UCXLSuccessResponse: """UCXL Standard Success Response Payload""" response: UCXLResponse def to_dict(self) -> Dict[str, Any]: """Convert to dictionary""" result = asdict(self) result['response']['code'] = self.response.code.value result['response']['timestamp'] = self.response.timestamp.isoformat() return result def to_json(self) -> str: """Convert to JSON string""" return json.dumps(self.to_dict()) @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'UCXLSuccessResponse': """Create from dictionary""" response_data = data['response'] response_data['timestamp'] = datetime.fromisoformat(response_data['timestamp'].replace('Z', '+00:00')) return cls(response=UCXLResponse(**response_data)) @classmethod def from_json(cls, json_str: str) -> 'UCXLSuccessResponse': """Create from JSON string""" return cls.from_dict(json.loads(json_str)) class UCXLErrorBuilder: """Builder for creating UCXL Error responses""" def __init__(self, code: UCXLErrorCode): self.code = code self.message = None self.details = {} self.source = "ucxl-api/v1" self.path = "/" self.request_id = f"req-{str(uuid.uuid4())[:8]}" self.cause = None def message(self, message: str) -> 'UCXLErrorBuilder': """Set a custom error message""" self.message = message return self def field(self, field: str, provided: Any) -> 'UCXLErrorBuilder': """Add details about the field that failed""" self.details["field"] = field self.details["provided"] = provided return self def expected_format(self, format_str: str) -> 'UCXLErrorBuilder': """Add expected format information""" self.details["expected_format"] = format_str return self def detail(self, key: str, value: Any) -> 'UCXLErrorBuilder': """Add a detail field""" self.details[key] = value return self def source(self, source: str) -> 'UCXLErrorBuilder': """Set the source service""" self.source = source return self def path(self, path: str) -> 'UCXLErrorBuilder': """Set the request path""" self.path = path return self def request_id(self, request_id: str) -> 'UCXLErrorBuilder': """Set the request ID""" self.request_id = request_id return self def cause(self, cause: str) -> 'UCXLErrorBuilder': """Set the error cause""" self.cause = cause return self def build(self) -> UCXLErrorResponse: """Build the error response""" message = self.message or self.code.default_message() details = self.details if self.details else None error = UCXLError( code=self.code, message=message, details=details, source=self.source, path=self.path, request_id=self.request_id, timestamp=datetime.now(timezone.utc), cause=self.cause, ) return UCXLErrorResponse(error=error) class UCXLResponseBuilder: """Builder for creating UCXL Success responses""" def __init__(self, code: UCXLResponseCode): self.code = code self.message = None self.data = None self.details = {} self.request_id = f"req-{str(uuid.uuid4())[:8]}" def message(self, message: str) -> 'UCXLResponseBuilder': """Set a custom success message""" self.message = message return self def data(self, data: Any) -> 'UCXLResponseBuilder': """Set the response data""" self.data = data return self def detail(self, key: str, value: Any) -> 'UCXLResponseBuilder': """Add a detail field""" self.details[key] = value return self def request_id(self, request_id: str) -> 'UCXLResponseBuilder': """Set the request ID""" self.request_id = request_id return self def build(self) -> UCXLSuccessResponse: """Build the success response""" message = self.message or self.code.default_message() details = self.details if self.details else None response = UCXLResponse( code=self.code, message=message, data=self.data, details=details, request_id=self.request_id, timestamp=datetime.now(timezone.utc), ) return UCXLSuccessResponse(response=response) # Convenience functions for common error patterns def create_invalid_address_error(address: str, path: str = "/", request_id: Optional[str] = None) -> UCXLErrorResponse: """Create a standardized invalid address error""" builder = UCXLErrorBuilder(UCXLErrorCode.INVALID_ADDRESS) builder.field("address", address) builder.expected_format("ucxl://:@:[//]") builder.path(path) builder.cause("parse_error") if request_id: builder.request_id(request_id) return builder.build() def create_not_found_error(resource_id: str, path: str = "/", request_id: Optional[str] = None) -> UCXLErrorResponse: """Create a standardized not found error""" builder = UCXLErrorBuilder(UCXLErrorCode.NOT_FOUND) builder.field("resource_id", resource_id) builder.path(path) if request_id: builder.request_id(request_id) return builder.build() def create_missing_field_error(field: str, path: str = "/", request_id: Optional[str] = None) -> UCXLErrorResponse: """Create a standardized missing field error""" builder = UCXLErrorBuilder(UCXLErrorCode.MISSING_FIELD) builder.message(f"Required field '{field}' is missing") builder.field(field, None) builder.path(path) builder.cause("validation_error") if request_id: builder.request_id(request_id) return builder.build() def create_success_response(data: Any = None, path: str = "/", request_id: Optional[str] = None) -> UCXLSuccessResponse: """Create a standardized success response""" builder = UCXLResponseBuilder(UCXLResponseCode.OK) if data is not None: builder.data(data) if request_id: builder.request_id(request_id) return builder.build() def create_created_response(resource_id: str, location: str, path: str = "/", request_id: Optional[str] = None) -> UCXLSuccessResponse: """Create a standardized resource created response""" builder = UCXLResponseBuilder(UCXLResponseCode.CREATED) builder.data({"id": resource_id, "url": location}) builder.detail("location", location) if request_id: builder.request_id(request_id) return builder.build() # Example usage and testing if __name__ == "__main__": # Example error response error = UCXLErrorBuilder(UCXLErrorCode.INVALID_ADDRESS) \ .message("Test error message") \ .field("address", "ucxl://invalid") \ .expected_format("ucxl://:@:") \ .source("test-service") \ .path("/test") \ .cause("parse_error") \ .build() print("Error Response JSON:") print(error.to_json()) print() # Example success response success = UCXLResponseBuilder(UCXLResponseCode.OK) \ .message("Test success") \ .data({"test": "value"}) \ .detail("info", "additional info") \ .build() print("Success Response JSON:") print(success.to_json()) print() # Test serialization round-trip error_json = error.to_json() error_restored = UCXLErrorResponse.from_json(error_json) print("Error serialization test:", error.error.code == error_restored.error.code) success_json = success.to_json() success_restored = UCXLSuccessResponse.from_json(success_json) print("Success serialization test:", success.response.code == success_restored.response.code)