from enum import Enum from typing import Any, Dict, List, Optional, Text, Type from django.core.exceptions import PermissionDenied class AbstractEnum(Enum): '''An enumeration whose members are used strictly for their names.''' def __new__(cls: Type['AbstractEnum']) -> 'AbstractEnum': obj = object.__new__(cls) obj._value_ = len(cls.__members__) + 1 return obj # Override all the `Enum` methods that use `_value_`. def __repr__(self) -> str: return str(self) def value(self) -> None: assert False def __reduce_ex__(self, proto: int) -> None: assert False class ErrorCode(AbstractEnum): BAD_REQUEST = () # Generic name, from the name of HTTP 400. REQUEST_VARIABLE_MISSING = () REQUEST_VARIABLE_INVALID = () BAD_IMAGE = () REALM_UPLOAD_QUOTA = () BAD_NARROW = () UNAUTHORIZED_PRINCIPAL = () BAD_EVENT_QUEUE_ID = () CSRF_FAILED = () INVITATION_FAILED = () INVALID_ZULIP_SERVER = () class JsonableError(Exception): '''A standardized error format we can turn into a nice JSON HTTP response. This class can be invoked in several ways. * Easiest, but completely machine-unreadable: raise JsonableError(_("No such widget: {}").format(widget_name)) The message may be passed through to clients and shown to a user, so translation is required. Because the text will vary depending on the user's language, it's not possible for code to distinguish this error from others in a non-buggy way. * Partially machine-readable, with an error code: raise JsonableError(_("No such widget: {}").format(widget_name), ErrorCode.NO_SUCH_WIDGET) Now the error's `code` attribute can be used, both in server and client code, to identify this type of error. The data (here, the widget name) is still embedded inside a translated string, and can't be accessed by code. * Fully machine-readable, with an error code and structured data: class NoSuchWidgetError(JsonableError): code = ErrorCode.NO_SUCH_WIDGET data_fields = ['widget_name'] def __init__(self, widget_name: str) -> None: self.widget_name = widget_name # type: str @staticmethod def msg_format() -> str: return _("No such widget: {widget_name}") raise NoSuchWidgetError(widget_name) Now both server and client code see a `widget_name` attribute. Subclasses may also override `http_status_code`. ''' # Override this in subclasses, or just pass a `code` argument # to the JsonableError constructor. code = ErrorCode.BAD_REQUEST # type: ErrorCode # Override this in subclasses if providing structured data. data_fields = [] # type: List[str] # Optionally override this in subclasses to return a different HTTP status, # like 403 or 404. http_status_code = 400 # type: int def __init__(self, msg: Text, code: Optional[ErrorCode]=None) -> None: if code is not None: self.code = code # `_msg` is an implementation detail of `JsonableError` itself. self._msg = msg # type: Text @staticmethod def msg_format() -> Text: '''Override in subclasses. Gets the items in `data_fields` as format args. This should return (a translation of) a string literal. The reason it's not simply a class attribute is to allow translation to work. ''' # Secretly this gets one more format arg not in `data_fields`: `_msg`. # That's for the sake of the `JsonableError` base logic itself, for # the simplest form of use where we just get a plain message string # at construction time. return '{_msg}' # # Infrastructure -- not intended to be overridden in subclasses. # @property def msg(self) -> Text: format_data = dict(((f, getattr(self, f)) for f in self.data_fields), _msg=getattr(self, '_msg', None)) return self.msg_format().format(**format_data) @property def data(self) -> Dict[str, Any]: return dict(((f, getattr(self, f)) for f in self.data_fields), code=self.code.name) def to_json(self) -> Dict[str, Any]: d = {'result': 'error', 'msg': self.msg} d.update(self.data) return d def __str__(self) -> str: return self.msg class RateLimited(PermissionDenied): def __init__(self, msg: str="") -> None: super().__init__(msg)