|
| 1 | +import http |
1 | 2 | import typing as t |
2 | 3 |
|
3 | 4 | from starlette import status |
4 | | -from starlette.exceptions import HTTPException as StarletteHTTPException |
5 | 5 |
|
6 | 6 |
|
7 | | -@t.no_type_check |
8 | | -def _get_error_details( |
9 | | - data: t.Union[t.List, t.Dict, "ErrorDetail"], |
10 | | - default_code: t.Optional[t.Union[str, int]] = None, |
11 | | -) -> t.Union[t.List["ErrorDetail"], "ErrorDetail", t.Dict[t.Any, "ErrorDetail"]]: |
12 | | - """ |
13 | | - Descend into a nested data structure, forcing any |
14 | | - lazy translation strings or strings into `ErrorDetail`. |
15 | | - """ |
16 | | - if isinstance(data, list): |
17 | | - ret = [_get_error_details(item, default_code) for item in data] |
18 | | - return ret |
19 | | - elif isinstance(data, dict): |
20 | | - ret = { |
21 | | - key: _get_error_details(value, default_code) for key, value in data.items() |
22 | | - } |
23 | | - return ret |
| 7 | +class APIException(Exception): |
| 8 | + __slots__ = ("headers", "description", "detail", "http_status") |
24 | 9 |
|
25 | | - text = str(data) |
26 | | - code = getattr(data, "code", default_code) |
27 | | - return ErrorDetail(text, code) |
28 | | - |
29 | | - |
30 | | -@t.no_type_check |
31 | | -def _get_codes( |
32 | | - detail: t.Union[t.List, t.Dict, "ErrorDetail"] |
33 | | -) -> t.Union[str, t.Dict, t.List[t.Dict]]: |
34 | | - if isinstance(detail, list): |
35 | | - return [_get_codes(item) for item in detail] |
36 | | - elif isinstance(detail, dict): |
37 | | - return {key: _get_codes(value) for key, value in detail.items()} |
38 | | - return detail.code |
39 | | - |
40 | | - |
41 | | -@t.no_type_check |
42 | | -def _get_full_details( |
43 | | - detail: t.Union[t.List, t.Dict, "ErrorDetail"] |
44 | | -) -> t.Union[t.Dict, t.List[t.Dict]]: |
45 | | - if isinstance(detail, list): |
46 | | - return [_get_full_details(item) for item in detail] |
47 | | - elif isinstance(detail, dict): |
48 | | - return {key: _get_full_details(value) for key, value in detail.items()} |
49 | | - return {"message": detail, "code": detail.code} |
50 | | - |
51 | | - |
52 | | -class ErrorDetail(str): |
53 | | - """ |
54 | | - A string-like object that can additionally have a code. |
55 | | - """ |
56 | | - |
57 | | - code = None |
58 | | - |
59 | | - def __new__( |
60 | | - cls, string: str, code: t.Optional[t.Union[str, int]] = None |
61 | | - ) -> "ErrorDetail": |
62 | | - self = super().__new__(cls, string) |
63 | | - self.code = code |
64 | | - return self |
65 | | - |
66 | | - def __eq__(self, other: object) -> bool: |
67 | | - r = super().__eq__(other) |
68 | | - try: |
69 | | - return r and self.code == other.code # type: ignore |
70 | | - except AttributeError: |
71 | | - return r |
72 | | - |
73 | | - def __ne__(self, other: object) -> bool: |
74 | | - return not self.__eq__(other) |
75 | | - |
76 | | - def __repr__(self) -> str: |
77 | | - return "ErrorDetail(string=%r, code=%r)" % ( |
78 | | - str(self), |
79 | | - self.code, |
80 | | - ) |
81 | | - |
82 | | - def __hash__(self) -> t.Any: |
83 | | - return hash(str(self)) |
84 | | - |
85 | | - |
86 | | -class APIException(StarletteHTTPException): |
87 | | - status_code = status.HTTP_500_INTERNAL_SERVER_ERROR |
88 | | - default_detail = "A server error occurred." |
89 | | - default_code = "error" |
| 10 | + status_code = status.HTTP_400_BAD_REQUEST |
| 11 | + code = "bad_request" |
90 | 12 |
|
91 | 13 | def __init__( |
92 | 14 | self, |
93 | | - detail: t.Optional[t.Union[t.List, t.Dict, "ErrorDetail", str]] = None, |
94 | | - code: t.Optional[t.Union[str, int]] = None, |
95 | | - status_code: t.Optional[int] = None, |
96 | | - headers: t.Optional[t.Dict[str, t.Any]] = None, |
| 15 | + detail: t.Union[t.List, t.Dict, str] = None, |
| 16 | + description: str = None, |
| 17 | + headers: t.Dict[str, t.Any] = None, |
| 18 | + status_code: int = None, |
97 | 19 | ) -> None: |
98 | | - if detail is None: |
99 | | - detail = self.default_detail |
100 | | - if code is None: |
101 | | - code = self.default_code |
| 20 | + assert self.status_code |
| 21 | + self.status_code = status_code or self.status_code |
| 22 | + self.http_status = http.HTTPStatus(self.status_code) |
| 23 | + self.description = description |
102 | 24 |
|
103 | | - if status_code is None: |
104 | | - status_code = self.status_code |
| 25 | + if detail is None: |
| 26 | + detail = self.http_status.phrase |
105 | 27 |
|
106 | | - super(APIException, self).__init__( |
107 | | - status_code=status_code, |
108 | | - detail=_get_error_details(detail, code), |
109 | | - headers=headers, |
110 | | - ) |
| 28 | + self.detail = detail |
| 29 | + self.headers = headers |
111 | 30 |
|
112 | | - def get_codes( |
113 | | - self, |
114 | | - ) -> t.Union[str, t.Dict, t.List]: |
115 | | - """ |
116 | | - Return only the code part of the error details. |
117 | | - Eg. {"name": ["required"]} |
118 | | - """ |
119 | | - return _get_codes(t.cast(ErrorDetail, self.detail)) # type: ignore |
| 31 | + def __repr__(self) -> str: |
| 32 | + class_name = self.__class__.__name__ |
| 33 | + return f"{class_name}(status_code={self.status_code!r}, detail={self.detail!r})" |
120 | 34 |
|
121 | | - def get_full_details( |
122 | | - self, |
123 | | - ) -> t.Union[t.Dict, t.List[t.Dict]]: |
| 35 | + def get_full_details(self) -> t.Union[t.Dict, t.List[t.Dict]]: |
124 | 36 | """ |
125 | 37 | Return both the message & code parts of the error details. |
126 | | - Eg. {"name": [{"message": "This field is required.", "code": "required"}]} |
127 | 38 | """ |
128 | | - return _get_full_details(t.cast(ErrorDetail, self.detail)) # type: ignore |
| 39 | + return dict( |
| 40 | + detail=self.detail, |
| 41 | + code=self.code, |
| 42 | + description=self.description or self.http_status.description, |
| 43 | + ) |
| 44 | + |
| 45 | + def get_details(self) -> t.Dict: |
| 46 | + result = dict(detail=self.detail) |
| 47 | + if self.description: |
| 48 | + result.update(description=self.description) |
| 49 | + return result |
0 commit comments