"""Common exceptions that can be useful throughout the app.""" from dataclasses import dataclass from flask import jsonify from constants import http_status @dataclass class ErrorResponse: code: str message: str 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. """ detail = None code = None status_code = None extra = None def __init__(self, detail=None, code=None, status_code=None, extra=None): if detail: self.detail = detail if code: self.code = code if status_code: self.status_code = status_code if extra: self.extra = extra 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 return result def respond(self): """Generate JSON response with appropriate status code and body. Status code is taken from the status_code attribute of the instance. Body is generated by to_dict() method. Returns: flask.Response """ response = jsonify(self.to_dict()) response.status_code = self.status_code return response class BadRequest(APIError): """HTTP 400 Bad Request.""" detail = "Bad Request." code = "bad_request" status_code = http_status.HTTP_400_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.HTTP_401_UNAUTHORIZED class Forbidden(APIError): """HTTP 403 Forbidden.""" detail = "Forbidden." code = "forbidden" status_code = http_status.HTTP_403_FORBIDDEN class NotFound(APIError): """HTTP 404 Not Found.""" detail = "Not Found." code = "not_found" status_code = http_status.HTTP_404_NOT_FOUND class Conflict(APIError): """HTTP 409 Conflict.""" detail = "Conflict." code = "conflict" status_code = http_status.HTTP_409_CONFLICT class UnsupportedMediaType(APIError): """HTTP 415 Unsupported Media Type.""" detail = "Unsupported Media Type." code = "unsupported_media_type" status_code = http_status.HTTP_415_UNSUPPORTED_MEDIA_TYPE class UnprocessableEntity(APIError): """HTTP 422 Unprocessable Entity.""" detail = "Unprocessable Entity." code = "unprocessable_entity" status_code = http_status.HTTP_422_UNPROCESSABLE_ENTITY class TooManyRequests(APIError): """HTTP 429 Too Many Requests.""" detail = "Too Many Requests." code = "too_many_requests" status_code = http_status.HTTP_429_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.HTTP_502_BAD_GATEWAY class InternalServerError(APIError): """HTTP 500 Internal Server Error.""" detail = "Internal Server Error." code = "server_error" status_code = http_status.HTTP_500_INTERNAL_SERVER_ERROR 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.HTTP_503_SERVICE_UNAVAILABLE class RequestError(Exception): """Request error Exception class. Deprecated, use APIError instead. """ def __init__(self, message: str or dict, error_code: str = "bad_request", status: int = 400): """Constructor. Args: message (str or dict): Response message. error_code (str): Error code key. status (int): HTTP code. """ super().__init__() self.message = message self.error_code = error_code self.status = status def __str__(self): """Convert error to string.""" return str(self.message) class NotFoundError(RequestError): """Not Found error Exception class. Deprecated, use NotFound instead. """ def __init__(self, message: str = "{} not found.", name: str = None): """Constructor. Args: message (str): Response message. name (str): Name of an object that was not found. It is used with standard message as a part. """ super().__init__(message if name is None else message.format(name), error_code="not_found", status=404) class ValidationError(RequestError): """Validation error Exception class. Deprecated, use JsonValidationError instead. """ def __init__(self, message: str): """Constructor. Args: message (str/dict): Response message. """ super().__init__(message, error_code="invalid_data")