"""
The exceptions of the Smarter Python SDK.
Every exception that the SDK raises is a :class:`SmarterError`, so a caller can catch them all with
one ``except`` clause. This follows the Smarter platform's own rule (ADR-012) that exceptions
subclass one hierarchy.
The api errors mirror the http status that the Smarter api returned:
.. code-block:: text
SmarterError
├── SmarterConfigurationError the client has no api key or base url
├── SmarterValueError an argument is invalid, before any request is sent
├── ResourceNotFoundError a resource was not found, by the api (NotFoundError) or by find()
└── APIError
├── APIConnectionError the request never got a response
│ └── APITimeoutError
├── APIResponseValidationError the response does not match the SDK's model of it
└── APIStatusError the api returned a 4xx or 5xx status
├── BadRequestError 400
├── AuthenticationError 401
├── PermissionDeniedError 403
├── NotFoundError 404, also a ResourceNotFoundError
├── ConflictError 409
├── UnprocessableEntityError 422
├── RateLimitError 429
└── InternalServerError 5xx
"""
from typing import TYPE_CHECKING, Optional
import httpx
if TYPE_CHECKING:
from smarter.models.journal import SAMJournalApiErrorBody
[docs]
class SmarterError(Exception):
"""The base class of every exception that the Smarter SDK raises."""
[docs]
class SmarterConfigurationError(SmarterError):
"""The client is missing configuration that it needs, e.g. an api key or a base url."""
[docs]
class SmarterValueError(SmarterError, ValueError):
"""An argument is invalid.
Raised before a request is sent.
"""
[docs]
class ResourceNotFoundError(SmarterError, LookupError):
"""A resource was not found: the api answered 404 (:class:`NotFoundError`), or ``find()`` found no resource of that name."""
[docs]
class APIError(SmarterError):
"""
A request to the Smarter api failed.
:param message: A human-readable description of the error.
:param request: The http request that failed.
:param body: The response's parsed JSON body, or its text, or None when there is no response.
"""
def __init__(self, message: str, request: httpx.Request, *, body: object = None) -> None:
super().__init__(message)
self.message = message
self.request = request
self.body = body
[docs]
class APIConnectionError(APIError):
"""The request did not get a response, e.g. the server is unreachable."""
def __init__(self, request: httpx.Request, message: str = "Connection error.") -> None:
super().__init__(message, request)
[docs]
class APITimeoutError(APIConnectionError):
"""The request timed out."""
def __init__(self, request: httpx.Request) -> None:
super().__init__(request, message="Request timed out.")
[docs]
class APIResponseValidationError(APIError):
"""
The api's response does not match the SDK's model of it.
This usually means that the SDK and the Smarter platform are of incompatible versions.
:param response: The http response.
:param body: The response's parsed JSON body.
:param message: A description of the mismatch.
"""
def __init__(self, response: httpx.Response, body: object, message: str) -> None:
super().__init__(message, response.request, body=body)
self.response = response
self.status_code = response.status_code
[docs]
class APIStatusError(APIError):
"""
The api returned a 4xx or 5xx status.
:param message: The error's description, from the response when it has one.
:param response: The http response.
:param body: The response's parsed JSON body, or its text.
:param error: The Smarter Journal error body, when the response is a journaled error.
"""
def __init__(
self,
message: str,
*,
response: httpx.Response,
body: object,
error: Optional["SAMJournalApiErrorBody"] = None,
) -> None:
super().__init__(message, response.request, body=body)
self.response = response
self.status_code = response.status_code
self.error = error
@property
def error_class(self) -> Optional[str]:
"""The name of the server-side exception class, e.g. ``SAMBrokerErrorNotFound``, when the platform sent it."""
return self.error.errorClass if self.error else None
def __str__(self) -> str:
return f"Error code: {self.status_code} - {self.message}"
[docs]
class BadRequestError(APIStatusError):
"""400: the request is invalid, e.g. a malformed manifest."""
[docs]
class AuthenticationError(APIStatusError):
"""401: the api key is missing, invalid, or inactive."""
[docs]
class PermissionDeniedError(APIStatusError):
"""403: the api key's user may not perform the operation."""
[docs]
class NotFoundError(APIStatusError, ResourceNotFoundError):
"""404: the resource does not exist, or the api key's user may not read it."""
[docs]
class ConflictError(APIStatusError):
"""409: the request conflicts with the resource's current state."""
[docs]
class UnprocessableEntityError(APIStatusError):
"""422: the request is well-formed but cannot be processed."""
[docs]
class RateLimitError(APIStatusError):
"""429: too many requests."""
[docs]
class InternalServerError(APIStatusError):
"""5xx: the platform failed to process the request."""
__all__ = [
"APIConnectionError",
"APIError",
"APIResponseValidationError",
"APIStatusError",
"APITimeoutError",
"AuthenticationError",
"BadRequestError",
"ConflictError",
"InternalServerError",
"NotFoundError",
"PermissionDeniedError",
"RateLimitError",
"ResourceNotFoundError",
"SmarterConfigurationError",
"SmarterError",
"SmarterValueError",
"UnprocessableEntityError",
]