Skip to content

Instantly share code, notes, and snippets.

@ktmud
Last active July 14, 2020 16:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ktmud/59ccbdf6c96570e1ffe41ee8e5ecd479 to your computer and use it in GitHub Desktop.
Save ktmud/59ccbdf6c96570e1ffe41ee8e5ecd479 to your computer and use it in GitHub Desktop.
API Design in Python/Flask - Generic Exceptions with Error Codes
from enum import Enum, IntEnum
from typing import Any, Dict, Optional
from flask import jsonify
from flask_babel import gettext as _, ngettext
from superset.typing import FlaskResponse
class SupersetErrorCode(IntEnum):
# Generic Superset errors
generic = 1000 # Generic error from Superset itself
db_error = 1001 # Error querying Superset database
create_failed = 1011 # Failed to create a record
update_failed = 1012 # Failed to edit a record
delete_failed = 1013 # Failed to delete a record
celery_timeout = 1100 # Unable to talke to celery
# Db engine errors
datasource_error = 2000 # Generic Error from datasource
datasource_query_timeout = 2100 # timeout when querying datasource
datasource_table_too_large = 2101
datasource_overload = 2102
# client request invalid
invalid_request = 4000 # invalid client side request
client_network_error = 4001 # reserved for frontend: error making requests
client_timeout = 4002 # reserved for frontend: timeout requesting superset api
access_denied = 4300
missing_csrf = 4301 # missing csrf token
no_access_to_datasource = 4310 # user has no access to datasource
no_edit_access_to_datasource = 4311 # user has no access to edit a datasource
no_access_to_table = 4321
no_edit_access_to_table = 4321
datasource_not_found = 4401
table_not_found = 4402
chart_not_found = 4403
# chart errors
chart_error = 10000 # error loading data for chart
unknown_chart_type = 10001
unknown_datasource_type = 10002
chart_datasource_wont_load = 10001 # can't load datasource for chart
class SupersetAlertLevel(Enum):
warning = "warning"
error = "error"
info = "info"
class SupersetAlert:
"""Superset alert gessage for client display and better logging"""
level = SupersetAlertLevel.error
code = SupersetErrorCode.generic # base error code
def __init__(
self,
code: Optional[SupersetErrorCode] = None,
message: Optional[str] = None,
**extra: Any,
):
# Enforce code range if we want...
# Make sure specific error code in in the same thousands range of the base code
if code and code - self.code > 1000:
raise RuntimeError(
f"Invalid error code for <{self.__class__.__name__}>, "
f"error code must in the range between "
f"{self.code} and {round(self.code, -3) + 999}"
)
self._message = message
self.code = code or self.code # override error code if needed
self.name = SupersetErrorCode(self.code).name
self.extra = extra
def to_json(self) -> Dict[str, Any]:
error = {
"error": self.name.upper(),
"code": self.code,
"message": self.message,
"level": self.level,
}
if self.message:
error["message"] = self.locale_message
if self.extra:
error["extra"] = self.extra
return error
@property
def message(self) -> str:
return self._message or f"{self.level}.{self.name}.message"
@property
def locale_message(self) -> str:
return _(self.message, **self.extra)
class SupersetException(Exception, SupersetAlert):
"""
Generic SupersetException. All raised exceptions should have level = error
and block API from returning a result.
"""
status = 500
def __init__(
self,
code: Optional[SupersetErrorCode] = None,
message: Optional[str] = None,
**extra: Any,
) -> None:
SupersetAlert.__init__(self, code=code, message=message, **extra)
Exception.__init__(self, self.message)
class SupersetSecurityException(SupersetException):
"""
Security exception where user access is denided.
"""
status = 401
code = SupersetErrorCode.access_denied
class SupersetTimeoutError(SupersetException):
"""
Security exception where user access is denided.
"""
code = SupersetErrorCode.datasource_query_timeout
timeout: Optional[int] = None
def __init__(
self,
code: Optional[SupersetErrorCode] = None,
message: Optional[str] = None,
timeout: Optional[int] = None,
**extra: Any,
) -> None:
super().__init__(code=code, message=message)
self.extra = {"timeout": timeout or self.timeout}
@property
def locale_message(self) -> str:
"""Apply extras on locale messages with plurals. Example:
SupersetAlert(message="Timeout after %(sec)s seconds", extra="")
"""
timeout: int = self.extra["timeout"]
return ngettext(self.message, f"{self.message}.pl", num=timeout, **self.extra)
def json_error_response(error: SupersetException) -> FlaskResponse:
return jsonify({"error": error.to_json()})
@ktmud
Copy link
Author

ktmud commented Jul 14, 2020

Can I just say lower cases are so much easier to read than all caps.... None of the examples in Python doc actually uses ALL_CAP attributes for Enums.

@etr2460
Copy link

etr2460 commented Jul 14, 2020

Hmm, interesting, i see ALL_CAPs attributes for enums here: https://docs.python.org/3/library/enum.html#creating-an-enum

@ktmud
Copy link
Author

ktmud commented Jul 14, 2020

Oops... Looks like I was reading some outdated docs: https://docs.python.org/3.5/library/enum.html#creating-an-enum . Never mind, then

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment