Last active
July 14, 2020 16:38
-
-
Save ktmud/59ccbdf6c96570e1ffe41ee8e5ecd479 to your computer and use it in GitHub Desktop.
API Design in Python/Flask - Generic Exceptions with Error Codes
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}) |
Hmm, interesting, i see ALL_CAPs attributes for enums here: https://docs.python.org/3/library/enum.html#creating-an-enum
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
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.