4
4
import aiohttp
5
5
from loguru import logger
6
6
7
+ from permit import ErrorDetails , HTTPValidationError
8
+
9
+ DEFAULT_SUPPORT_LINK = "https://permit-io.slack.com/ssb/redirect"
10
+
7
11
8
12
class PermitException (Exception ): # noqa: N818
9
13
"""Permit base exception"""
@@ -38,18 +42,27 @@ class PermitContextChangeError(Exception):
38
42
39
43
class PermitApiError (Exception ):
40
44
"""
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.
42
46
"""
43
47
44
48
def __init__ (
45
49
self ,
46
- message : str ,
47
50
response : aiohttp .ClientResponse ,
48
- response_json : Optional [dict ] = None ,
51
+ body : Optional [dict ] = None ,
49
52
):
50
- super ().__init__ (message )
53
+ super ().__init__ ()
51
54
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 ()
53
66
54
67
@property
55
68
def response (self ) -> aiohttp .ClientResponse :
@@ -69,7 +82,7 @@ def details(self) -> Optional[dict]:
69
82
Returns:
70
83
The HTTP response json. If no content will return None.
71
84
"""
72
- return self ._response_json
85
+ return self ._body
73
86
74
87
@property
75
88
def request_url (self ) -> str :
@@ -102,21 +115,110 @@ def content_type(self) -> Optional[str]:
102
115
return self ._response .headers .get ("content-type" )
103
116
104
117
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
+
105
198
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 :
118
203
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
120
222
121
223
122
224
def handle_client_error (func ):
0 commit comments