from http import HTTPStatus as http_status from typing import Any class APIError(Exception): """ Base class for error API responses. Supposed to be raised in API views and handled by a handler registered in the app or in a blueprint. This class can be used directly, but more convenient is to subclass it for each type of error and override code, status_code and detail attributes. Using this class and its subclasses is preferred over abort() function, because it enforces consistent response format across all endpoints. Attributes: detail (str): Description that clarifies the cause of the error. It is addressed to developers who integrate the API with another application (e.g. web front-end), but not to the end user, because it can contain technical details. code (str): Unique identifier of an error. API consumer can use it to unambiguously identify the error and take appropriate actions, e.g. show human readable message to the end user on front-end. status_code (int): HTTP status code that must be used in response by error handler. extra: Optional arbitrary JSON-serializable structure containing data associated with the error. In case of validation failure it can be a mapping between field names and corresponding errors. In most of the cases however this attribute is None. service: The error happened in this service. original_status_code: Original other service error status code. original_response: Other service error body. """ detail: str = None code: str = None headers: dict = None status_code: int = None extra: Any = None service: str = None original_status_code: int = None original_response: Any = None ORIGINAL_STATUS_CODES_TO_BYPASS = (404,) def __init__( self, detail: str = None, code: str = None, status_code: int = None, extra: Any = None, headers: dict = None, service: str = None, original_status_code: int = None, original_response: Any = None, ): """Init API error. Args: detail: Error detailed description. code: Error string code. status_code: Error status code. extra: Some extra data. headers: Error headers. service: Error from this service. original_status_code: Original other service error status code. original_response: Other service error body. """ if original_status_code and original_status_code in self.ORIGINAL_STATUS_CODES_TO_BYPASS: self.status_code = original_status_code elif status_code: self.status_code = status_code if detail: self.detail = detail if code: self.code = code if extra: self.extra = extra if headers: self.headers = headers if service: self.service = service if original_status_code: self.original_status_code = original_status_code if original_response: self.original_response = original_response def __str__(self): return f"<[{self.status_code}] {self.__class__.__name__}>: {self.to_dict()}" def to_dict(self): """ Serialize exception instance to a dictionary. Primary use case is to prepare exception for dumping it to JSON and adding to response body. Resulting dictionary always contains `code` and `detail` attributes. `extra` attribute is also added if it is not None. Returns: dict: Exception serialized to a dictionary. """ result = {"code": self.code, "detail": self.detail} if self.extra: result["extra"] = self.extra if self.service or self.original_status_code or self.original_response: result["original"] = { **({"service": self.service} if self.service else {}), **({"status_code": self.original_status_code} if self.original_status_code else {}), **({"response": self.original_response} if self.original_response else {}), } return result class BadRequest(APIError): """ HTTP 400 Bad Request. """ detail = "Bad Request." code = "bad_request" status_code = http_status.BAD_REQUEST class JsonValidationError(BadRequest): """ Raised if incoming JSON payload does not pass validation. """ detail = "Invalid JSON payload." code = "validation_error.json" class QueryParamsValidationError(BadRequest): """ Raised if query string parameters validation failed. """ detail = "Invalid query parameters." code = "validation_error.query_params" class Unauthorized(APIError): """ HTTP 401 Unauthorized. """ detail = "Unauthorized." code = "unauthorized" status_code = http_status.UNAUTHORIZED class NotFound(APIError): """ HTTP 404 Not Found. """ detail = "Not Found." code = "not_found" status_code = http_status.NOT_FOUND class Conflict(APIError): """ HTTP 409 Conflict. """ detail = "Conflict." code = "conflict" status_code = http_status.CONFLICT class UnsupportedMediaType(APIError): """ HTTP 415 Unsupported Media Type. """ detail = "Unsupported Media Type." code = "unsupported_media_type" status_code = http_status.UNSUPPORTED_MEDIA_TYPE class UnprocessableEntity(APIError): """ HTTP 422 Unprocessable Entity. """ detail = "Unprocessable Entity." code = "unprocessable_entity" status_code = http_status.UNPROCESSABLE_ENTITY class TooManyRequests(APIError): """ HTTP 429 Too Many Requests. """ detail = "Too Many Requests." code = "too_many_requests" status_code = http_status.TOO_MANY_REQUESTS class BadGateway(APIError): """ HTTP 502 Bad Gateway. This exception may be raised if a third party API returns invalid response. """ detail = "Bad Gateway." code = "bad_gateway" status_code = http_status.BAD_GATEWAY class ServiceUnavailable(APIError): """ HTTP 503 Service Unavailable. This exception is usually raised when a third party API we rely on is unavailable. """ detail = "Service Unavailable." code = "service_unavailable" status_code = http_status.SERVICE_UNAVAILABLE class UnsupportedCacheMode(APIError): """ HTTP 400 Bad Request. """ detail = "Unsupported cache mode." code = "unsupported_cache_mode" status_code = http_status.BAD_REQUEST class ClientAPIError(BadGateway): """Base class for API client related errors.""" class APIUnauthorized(ClientAPIError): """Raised if request to API failed with authorization error. This exception possibly means that app configuration is incorrect. """ detail: str = "Authentication failed" code = "unauthorized" status_code: int = http_status.UNAUTHORIZED class APIMisconfigured(ClientAPIError): """Raised if API configuration is incorrect.""" detail: str = "Please set all necessary client settings" class APIUnavailable(ClientAPIError): """Raised if connection to API cannot be established or timeout exceeded.""" detail: str = "Service is unavailable" class APIInvalidResponse(ClientAPIError): """Raised if API returned response which can not be handled.""" detail: str = "Invalid response"