"""BriteCore custom exceptions.
v2.0.0 Enhanced Error Model
=============================
All exceptions now include structured metadata for better error handling:
- **status_code**: HTTP status code (e.g., 400, 404, 500)
- **error_code**: BriteCore error code (e.g., "quote_not_found", "invalid_field")
- **request_id**: Request correlation ID for debugging (e.g., "abc123def456")
- **detail**: Human-readable error message
- **raw_payload**: Full server response dict for debugging
- **sanitized_body**: Redacted request body for safe debugging
Example:
try:
retrieve_quote(quote_number="INVALID", client=client)
except NotFoundError as e:
print(f"Status: {e.status_code}") # 404
print(f"Code: {e.error_code}") # "quote_not_found"
print(f"Request ID: {e.request_id}") # "abc123def456"
print(f"Detail: {e.detail}") # "Quote INVALID does not exist"
print(f"Raw: {e.raw_payload}") # {"success": false, ...}
"""
from typing import Any
[docs]
class BritecoreError:
"""Namespace for custom exceptions related to BriteCore operations."""
[docs]
class Base(Exception):
"""Base class for all SDK-originated exceptions.
v2.0.0 Enhanced with structured metadata:
- status_code: HTTP status code
- error_code: BriteCore error code
- request_id: Request correlation ID
- detail: Human-readable error message
- raw_payload: Full server response
- hint: Optional troubleshooting suggestion
"""
def __init__(
self,
message: str,
*,
status_code: int = 500,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
hint: str | None = None,
) -> None:
self.message = message
self.detail = message # Alias for clarity
self.status_code = status_code
self.error_code = error_code
self.request_id = request_id
self.raw_payload = raw_payload or {}
self.sanitized_body = sanitized_body
self.hint = hint
super().__init__(self.message)
def __str__(self) -> str:
parts = [str(self.message)]
if self.request_id:
parts.append(f"[Request-ID: {self.request_id}]")
if self.error_code:
parts.append(f"[Error Code: {self.error_code}]")
if self.hint:
parts.append(f"\n💡 Hint: {self.hint}")
return " ".join(parts)
[docs]
class NoDataReturned(Base):
"""Raised when BriteCore API returns no usable data."""
def __init__(
self,
message: str,
request: str | None = None,
http_error: str | None = None,
endpoint: str | None = None,
http_status: int | None = None,
*,
status_code: int | None = None,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
) -> None:
self.request = request
self.http_error = http_error
self.endpoint = endpoint
self.http_status = http_status
# v2.0.0: Use status_code parameter, fall back to http_status
resolved_status = status_code or http_status or 500
super().__init__(
message,
status_code=resolved_status,
error_code=error_code,
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
)
def __str__(self) -> str:
parts = [f"No data returned - {self.message}"]
if self.endpoint:
parts.append(f"Endpoint: {self.endpoint}")
if self.http_status:
parts.append(f"HTTP Status: {self.http_status}")
if self.error_code:
parts.append(f"Error Code: {self.error_code}")
if self.request_id:
parts.append(f"Request-ID: {self.request_id}")
if self.request:
parts.append(f"Request: {self.request}")
if self.http_error:
parts.append(f"HTTP Error: {self.http_error}")
return "\n".join(parts)
[docs]
class NoTokenReturned(Base):
"""Raised when OAuth token request fails."""
def __init__(
self,
message: str,
request: str | None = None,
http_error: str | None = None,
http_status: int | None = None,
*,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
) -> None:
self.request = request
self.http_error = http_error
self.http_status = http_status
resolved_status = http_status or 500
super().__init__(
message,
status_code=resolved_status,
error_code=error_code,
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
)
def __str__(self) -> str:
parts = [
f"BriteCore was unable to return any authorization token - {self.message}"
]
if self.http_status:
parts.append(f"HTTP Status: {self.http_status}")
if self.error_code:
parts.append(f"Error Code: {self.error_code}")
if self.request_id:
parts.append(f"Request-ID: {self.request_id}")
if self.request:
parts.append(f"Request: {self.request}")
if self.http_error:
parts.append(f"HTTP Error: {self.http_error}")
return "\n".join(parts)
[docs]
class InvalidPhoneNumber(Base):
"""Raised when phone number validation fails."""
def __str__(self) -> str:
return f"Invalid Phone Number - {self.message}"
[docs]
class InvalidEmailAddress(Base):
"""Raised when email address validation fails."""
def __str__(self) -> str:
return f"Invalid E-Mail Address - {self.message}"
[docs]
class InvalidAddress(Base):
"""Raised when address validation fails."""
def __str__(self) -> str:
return f"Invalid Address - {self.message}"
[docs]
class BritecoreKeyError(Base):
"""Raised when a required key is missing."""
[docs]
class NoSiteError(Base):
"""Raised when no target site is assigned."""
[docs]
class MissingParameter(Base):
"""Raised when a required parameter is missing."""
[docs]
class ConflictingParameters(Base):
"""Raised when multiple conflicting parameters are specified."""
[docs]
class AuthenticationError(Base):
"""Raised when API authentication fails (invalid key, expired token, 401/403)."""
def __init__(
self,
message: str,
http_status: int | None = None,
endpoint: str | None = None,
*,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
hint: str | None = None,
) -> None:
self.http_status = http_status
self.endpoint = endpoint
# Auto-generate hints
if not hint:
hint = self._generate_hint(http_status, message)
resolved_status = http_status or 401
super().__init__(
message,
status_code=resolved_status,
error_code=error_code or "authentication_failed",
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
hint=hint,
)
@staticmethod
def _generate_hint(status_code: int | None, message: str) -> str | None:
"""Generate helpful hints for authentication errors."""
if status_code == 401:
return (
"Credentials are invalid or expired. "
"Verify api_key is correct or OAuth token is fresh. "
"Run: britecore-check-config"
)
elif status_code == 403:
return (
"Credentials are valid but lack permission for this resource. "
"Check your user role and access level in BriteCore admin."
)
elif "token" in message.lower():
return (
"OAuth token issue. Ensure client_id and client_secret are correct and "
"that the OAuth token endpoint is accessible."
)
elif "expired" in message.lower():
return (
"Your authentication credentials or token have expired. "
"Update credentials in ~/.britecore/.secrets.toml and retry."
)
return None
def __str__(self) -> str:
parts = ["BriteCore authentication failed"]
if self.http_status:
parts[0] += f" (HTTP {self.http_status})"
parts[0] += f" - {self.message}"
if self.error_code:
parts.append(f"Error Code: {self.error_code}")
if self.request_id:
parts.append(f"Request-ID: {self.request_id}")
if self.endpoint:
parts.append(f"Endpoint: {self.endpoint}")
if self.hint:
parts.append(f"💡 Hint: {self.hint}")
return "\n".join(parts)
[docs]
class RateLimitError(Base):
"""Raised when the API rate limit is exceeded (HTTP 429)."""
def __init__(
self,
message: str,
retry_after: int | None = None,
*,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
) -> None:
self.retry_after = retry_after
super().__init__(
message,
status_code=429,
error_code=error_code or "rate_limit_exceeded",
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
)
def __str__(self) -> str:
retry_info = (
f" Retry after {self.retry_after}s." if self.retry_after else ""
)
msg = f"BriteCore rate limit exceeded - {self.message}.{retry_info}"
if self.request_id:
msg += f" [Request-ID: {self.request_id}]"
return msg
[docs]
class ServerError(Base):
"""Raised when the API returns a 5xx server error."""
def __init__(
self,
message: str,
http_status: int | None = None,
endpoint: str | None = None,
*,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
) -> None:
self.http_status = http_status
self.endpoint = endpoint
resolved_status = http_status or 500
super().__init__(
message,
status_code=resolved_status,
error_code=error_code or "server_error",
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
)
def __str__(self) -> str:
parts = ["BriteCore server error"]
if self.http_status:
parts[0] += f" (HTTP {self.http_status})"
parts[0] += f" - {self.message}"
if self.error_code:
parts.append(f"Error Code: {self.error_code}")
if self.request_id:
parts.append(f"Request-ID: {self.request_id}")
if self.endpoint:
parts.append(f"Endpoint: {self.endpoint}")
return "\n".join(parts)
[docs]
class ValidationError(NoDataReturned):
"""Raised when API validation fails (for example HTTP 400/422).
v2.0.0 Enhancement: includes validation_errors dict with field-level details.
"""
def __init__(
self,
message: str,
request: str | None = None,
http_error: str | None = None,
endpoint: str | None = None,
http_status: int | None = None,
*,
validation_errors: dict[str, Any] | None = None,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
) -> None:
self.validation_errors = validation_errors or {}
resolved_status = http_status or 400
super().__init__(
message,
request=request,
http_error=http_error,
endpoint=endpoint,
http_status=resolved_status,
status_code=resolved_status,
error_code=error_code or "validation_error",
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
)
def __str__(self) -> str:
base = super().__str__()
if self.validation_errors:
base += "\nValidation Errors:"
for field, errors in self.validation_errors.items():
if isinstance(errors, list):
for error in errors:
base += f"\n {field}: {error}"
else:
base += f"\n {field}: {errors}"
return base
[docs]
class NotFoundError(NoDataReturned):
"""Raised when an API resource is not found (HTTP 404)."""
[docs]
class ConflictError(NoDataReturned):
"""Raised when API returns a conflict (HTTP 409)."""
[docs]
class ConfigurationError(Base):
"""Raised when the client is misconfigured (missing base_url, api_key, etc.)."""
def __init__(
self,
message: str,
*,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
hint: str | None = None,
) -> None:
# Auto-generate hints for common issues
if not hint:
hint = self._generate_hint(message)
super().__init__(
message,
status_code=400,
error_code=error_code or "configuration_error",
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
hint=hint,
)
@staticmethod
def _generate_hint(message: str) -> str | None:
"""Generate helpful hints for common configuration errors."""
msg_lower = message.lower()
if "base_url" in msg_lower:
return (
"Set base_url via: ~/.britecore/.secrets.toml[site_name] or "
"$env:BRITECORE_SDK_BASE_URL (Windows) / "
"$BRITECORE_SDK_BASE_URL (Linux)"
)
elif "api_key" in msg_lower:
return (
"Set api_key via: ~/.britecore/.secrets.toml[site_name] or "
"$env:BRITECORE_SDK_API_KEY (Windows) / "
"$BRITECORE_SDK_API_KEY (Linux)"
)
elif "client_id" in msg_lower or "client_secret" in msg_lower:
return (
"For OAuth, set client_id and client_secret via: "
"~/.britecore/.secrets.toml[site_name] or env vars "
"BRITECORE_SDK_CLIENT_ID / BRITECORE_SDK_CLIENT_SECRET"
)
elif "target_site" in msg_lower:
return (
"Set target_site via: ~/.britecore/settings.toml [default] section or "
"$env:target_site (Windows) / $target_site (Linux)"
)
elif "credentials" in msg_lower:
return (
"Provide either: (1) api_key OR (2) client_id + client_secret. "
"See ~/.britecore/.secrets.toml template."
)
return None
def __str__(self) -> str:
msg = f"BriteCore configuration error - {self.message}"
if self.hint:
msg += f"\n💡 Hint: {self.hint}"
return msg
[docs]
class RequestTimeoutError(Base):
"""Raised when an API request exceeds its configured timeout."""
def __init__(
self,
message: str,
timeout_seconds: int | float | None = None,
endpoint: str | None = None,
*,
error_code: str | None = None,
request_id: str | None = None,
raw_payload: dict[str, Any] | None = None,
sanitized_body: Any | None = None,
) -> None:
self.timeout_seconds = timeout_seconds
self.endpoint = endpoint
super().__init__(
message,
status_code=408,
error_code=error_code or "request_timeout",
request_id=request_id,
raw_payload=raw_payload,
sanitized_body=sanitized_body,
)
def __str__(self) -> str:
parts = ["Request timeout"]
if self.timeout_seconds:
parts[0] += f" ({self.timeout_seconds}s)"
parts[0] += f" - {self.message}"
if self.error_code:
parts.append(f"Error Code: {self.error_code}")
if self.request_id:
parts.append(f"Request-ID: {self.request_id}")
if self.endpoint:
parts.append(f"Endpoint: {self.endpoint}")
return "\n".join(parts)
# ---------------------------------------------------------------------------
# Flat aliases — importable directly from britecore_sdk.exceptions
# so consumer code can write:
# from britecore_sdk.exceptions import NotFoundError
# instead of:
# except BritecoreError.NotFoundError
# ---------------------------------------------------------------------------
BritecoreBaseError = BritecoreError.Base
NoDataReturned = BritecoreError.NoDataReturned
NoTokenReturned = BritecoreError.NoTokenReturned
AuthenticationError = BritecoreError.AuthenticationError
RateLimitError = BritecoreError.RateLimitError
ServerError = BritecoreError.ServerError
ValidationError = BritecoreError.ValidationError
NotFoundError = BritecoreError.NotFoundError
ConflictError = BritecoreError.ConflictError
ConfigurationError = BritecoreError.ConfigurationError
RequestTimeoutError = BritecoreError.RequestTimeoutError
BritecoreKeyError = BritecoreError.BritecoreKeyError
NoSiteError = BritecoreError.NoSiteError
MissingParameter = BritecoreError.MissingParameter
ConflictingParameters = BritecoreError.ConflictingParameters
InvalidPhoneNumber = BritecoreError.InvalidPhoneNumber
InvalidEmailAddress = BritecoreError.InvalidEmailAddress
InvalidAddress = BritecoreError.InvalidAddress
__all__ = [
"BritecoreError",
# Flat aliases
"BritecoreBaseError",
"NoDataReturned",
"NoTokenReturned",
"AuthenticationError",
"RateLimitError",
"ServerError",
"ValidationError",
"NotFoundError",
"ConflictError",
"ConfigurationError",
"RequestTimeoutError",
"BritecoreKeyError",
"NoSiteError",
"MissingParameter",
"ConflictingParameters",
"InvalidPhoneNumber",
"InvalidEmailAddress",
"InvalidAddress",
]