Skip to content

Commit 75e002e

Browse files
committed
Added detailed exception types
1 parent d03b4bf commit 75e002e

File tree

1 file changed

+121
-19
lines changed

1 file changed

+121
-19
lines changed

permit/exceptions.py

Lines changed: 121 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
import aiohttp
55
from loguru import logger
66

7+
from permit import ErrorDetails, HTTPValidationError
8+
9+
DEFAULT_SUPPORT_LINK = "https://permit-io.slack.com/ssb/redirect"
10+
711

812
class PermitException(Exception): # noqa: N818
913
"""Permit base exception"""
@@ -38,18 +42,27 @@ class PermitContextChangeError(Exception):
3842

3943
class PermitApiError(Exception):
4044
"""
41-
Wraps an error HTTP Response that occured during a Permit REST API request.
45+
Wraps an error HTTP Response that occurred during a Permit REST API request.
4246
"""
4347

4448
def __init__(
4549
self,
46-
message: str,
4750
response: aiohttp.ClientResponse,
48-
response_json: Optional[dict] = None,
51+
body: Optional[dict] = None,
4952
):
50-
super().__init__(message)
53+
super().__init__()
5154
self._response = response
52-
self._response_json = response_json
55+
self._body = body
56+
57+
def _get_message(self) -> str:
58+
return f"{self.status_code} API Error: {self.details}"
59+
60+
def __str__(self):
61+
return self._get_message()
62+
63+
@property
64+
def message(self) -> str:
65+
return self._get_message()
5366

5467
@property
5568
def response(self) -> aiohttp.ClientResponse:
@@ -69,7 +82,7 @@ def details(self) -> Optional[dict]:
6982
Returns:
7083
The HTTP response json. If no content will return None.
7184
"""
72-
return self._response_json
85+
return self._body
7386

7487
@property
7588
def request_url(self) -> str:
@@ -102,21 +115,110 @@ def content_type(self) -> Optional[str]:
102115
return self._response.headers.get("content-type")
103116

104117

118+
class PermitValidationError(PermitApiError):
119+
"""
120+
Validation error response from the Permit API.
121+
"""
122+
123+
def __init__(self, response: aiohttp.ClientResponse, body: dict):
124+
self._content = HTTPValidationError.parse_obj(body)
125+
super().__init__(response, body)
126+
127+
def _get_message(self) -> str:
128+
message = "Validation error\n"
129+
for error in self.content.detail or []:
130+
location = " -> ".join(str(loc) for loc in error.loc)
131+
message += f"{location}\n\t{error.msg} ({error.type})\n"
132+
133+
return message
134+
135+
@property
136+
def content(self) -> HTTPValidationError:
137+
return self._content
138+
139+
140+
class PermitApiDetailedError(PermitApiError):
141+
"""
142+
Detailed error response from the Permit API.
143+
"""
144+
145+
def __init__(self, response: aiohttp.ClientResponse, body: dict):
146+
self._content = ErrorDetails.parse_obj(body)
147+
super().__init__(response, body)
148+
149+
def _get_message(self) -> str:
150+
message = f"{self.content.title} ({self.content.error_code})\n"
151+
if self.content.message:
152+
split_message = self.content.message.replace(". ", ".\n")
153+
message += f"{split_message}\n"
154+
message += f"For more information: {self.support_link} (Request ID: {self.id})"
155+
return message
156+
157+
@property
158+
def content(self) -> ErrorDetails:
159+
return self._content
160+
161+
@property
162+
def id(self) -> str:
163+
return self.content.id
164+
165+
@property
166+
def code(self) -> str:
167+
return self.content.error_code.value
168+
169+
@property
170+
def title(self) -> str:
171+
return self.content.title
172+
173+
@property
174+
def explanation(self) -> str:
175+
return self.content.message or "No further explanation provided"
176+
177+
@property
178+
def support_link(self) -> str:
179+
return str(self.content.support_link or DEFAULT_SUPPORT_LINK)
180+
181+
@property
182+
def additional_info(self):
183+
return self.content.additional_info
184+
185+
186+
class PermitAlreadyExistsError(PermitApiDetailedError):
187+
"""
188+
Object already exists response from the Permit API.
189+
"""
190+
191+
192+
class PermitNotFoundError(PermitApiDetailedError):
193+
"""
194+
Object not found response from the Permit API.
195+
"""
196+
197+
105198
async def handle_api_error(response: aiohttp.ClientResponse):
106-
if response.status < 200 or response.status >= 400:
107-
# handle non-json errors (can be returned by load balancer)
108-
content_type = response.headers.get("content-type")
109-
if content_type is not None and content_type.lower() != "application/json":
110-
error_string = await response.text()
111-
raise PermitApiError(
112-
f"{response.status} API Error",
113-
response,
114-
{"status_code": response.status, "error": error_string},
115-
)
116-
117-
# fallback to handle json errors
199+
if 200 <= response.status < 400:
200+
return
201+
202+
try:
118203
json = await response.json()
119-
raise PermitApiError(f"{response.status} API error", response, json)
204+
except aiohttp.ContentTypeError as e:
205+
text = await response.text()
206+
raise PermitApiError(response, {"details": text}) from e
207+
208+
try:
209+
if response.status == 422:
210+
raise PermitValidationError(response, json)
211+
elif response.status == 409:
212+
raise PermitAlreadyExistsError(response, json)
213+
elif response.status == 404:
214+
raise PermitNotFoundError(response, json)
215+
else:
216+
raise PermitApiDetailedError(response, json)
217+
except PermitApiError as e:
218+
raise e
219+
except Exception as e:
220+
logger.exception(f"Failed to create specific error class for status {response.status}: {e}")
221+
raise PermitApiError(response, json) from e
120222

121223

122224
def handle_client_error(func):

0 commit comments

Comments
 (0)