Skip to content

Commit 47ac6e3

Browse files
authored
Merge pull request #57 from mattmanley/improve_response_logging
Improve exceptions and fix JSON decoding bug
2 parents 4e642ee + f8a487f commit 47ac6e3

3 files changed

Lines changed: 91 additions & 44 deletions

File tree

maproulette/api/errors.py

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,34 +3,65 @@
33

44
class MapRouletteBaseException(Exception):
55
"""MapRoulette Base Exception"""
6-
def __init__(self, message, status=None, payload=None):
6+
def __init__(self, message, status, payload):
77
self.message = message
88
self.status = status
99
self.payload = payload
1010

1111
def __str__(self):
12-
return repr({
12+
error = {
1313
"status": self.status,
1414
"message": self.message,
1515
"payload": self.payload
16-
})
16+
}
17+
return repr({k: v for (k, v) in error.items() if v is not None})
1718

1819

1920
class NotFoundError(MapRouletteBaseException):
2021
"""Resource cannot be found"""
22+
def __init__(self, message=None, status=None, payload=None):
23+
if message:
24+
self.message = message
25+
else:
26+
self.message = "Resource cannot be found."
27+
super().__init__(message=self.message, status=status, payload=payload)
2128

2229

2330
class ConnectionUnavailableError(MapRouletteBaseException):
2431
"""A connection error occurred"""
32+
def __init__(self, message=None, status=None, payload=None):
33+
if message:
34+
self.message = message
35+
else:
36+
self.message = "A connection error occurred."
37+
super().__init__(message=self.message, status=status, payload=payload)
2538

2639

2740
class UnauthorizedError(MapRouletteBaseException):
2841
"""The user is not authorized to make this request"""
42+
def __init__(self, message=None, status=None, payload=None):
43+
if message:
44+
self.message = message
45+
else:
46+
self.message = "The user is not authorized to make this request."
47+
super().__init__(message=self.message, status=status, payload=payload)
2948

3049

3150
class HttpError(MapRouletteBaseException):
3251
"""An HTTP error occurred"""
52+
def __init__(self, message=None, status=None, payload=None):
53+
if message:
54+
self.message = message
55+
else:
56+
self.message = "An HTTP error occurred."
57+
super().__init__(message=self.message, status=status, payload=payload)
3358

3459

3560
class InvalidJsonError(MapRouletteBaseException):
3661
"""Errors produced from an invalid JSON object"""
62+
def __init__(self, message=None, status=None, payload=None):
63+
if message:
64+
self.message = message
65+
else:
66+
self.message = "Invalid JSON payload."
67+
super().__init__(message=self.message, status=status, payload=payload)

maproulette/api/maproulette_server.py

Lines changed: 40 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -64,28 +64,25 @@ def get(self, endpoint, params=None):
6464
except requests.exceptions.HTTPError as e:
6565
if e.response.status_code == 404:
6666
raise NotFoundError(
67-
message='Resource not found',
68-
status=e.response.status_code,
69-
payload=e.response
67+
message=self.parse_response_message(e.response),
68+
status=e.response.status_code
7069
) from None
7170
else:
7271
raise HttpError(
73-
message='An HTTP error occurred',
74-
status=e.response.status_code,
75-
payload=e.response
72+
message=self.parse_response_message(e.response),
73+
status=e.response.status_code
7674
) from None
7775
except (requests.ConnectionError, requests.Timeout) as e:
7876
raise ConnectionUnavailableError(
79-
message="Connection Unavailable",
80-
status=e.response.status_code,
81-
payload=response
77+
message=self.parse_response_message(e.response),
78+
status=e.response.status_code
8279
) from None
8380
try:
8481
return {
8582
"data": response.json(),
8683
"status": response.status_code
8784
}
88-
except json.decoder.JSONDecodeError:
85+
except ValueError:
8986
return {
9087
"status": response.status_code
9188
}
@@ -108,21 +105,18 @@ def post(self, endpoint, body=None, params=None):
108105
except requests.exceptions.HTTPError as e:
109106
if e.response.status_code == 400:
110107
raise InvalidJsonError(
111-
message='Invalid JSON payload',
112-
status=e.response.status_code,
113-
payload=e.response
108+
message=self.parse_response_message(e.response),
109+
status=e.response.status_code
114110
) from None
115111
elif e.response.status_code == 401:
116112
raise UnauthorizedError(
117-
message='The user is not authorized to make this request',
118-
status=e.response.status_code,
119-
payload=e.response
113+
message=self.parse_response_message(e.response),
114+
status=e.response.status_code
120115
) from None
121116
else:
122117
raise HttpError(
123-
message='An HTTP error occurred',
124-
status=e.response.status_code,
125-
payload=e.response
118+
message=self.parse_response_message(e.response),
119+
status=e.response.status_code
126120
) from None
127121
except (requests.ConnectionError, requests.Timeout) as e:
128122
raise ConnectionUnavailableError(e) from None
@@ -131,7 +125,7 @@ def post(self, endpoint, body=None, params=None):
131125
"data": response.json(),
132126
"status": response.status_code
133127
}
134-
except json.decoder.JSONDecodeError:
128+
except ValueError:
135129
return {
136130
"status": response.status_code
137131
}
@@ -154,21 +148,18 @@ def put(self, endpoint, body=None, params=None):
154148
except requests.exceptions.HTTPError as e:
155149
if e.response.status_code == 400:
156150
raise InvalidJsonError(
157-
message='Invalid JSON payload',
158-
status=e.response.status_code,
159-
payload=e.response
151+
message=self.parse_response_message(e.response),
152+
status=e.response.status_code
160153
) from None
161154
elif e.response.status_code == 401:
162155
raise UnauthorizedError(
163-
message='The user is not authorized to make this request',
164-
status=e.response.status_code,
165-
payload=e.response
156+
message=self.parse_response_message(e.response),
157+
status=e.response.status_code
166158
) from None
167159
else:
168160
raise HttpError(
169-
message='An HTTP error occurred',
170-
status=e.response.status_code,
171-
payload=e.response
161+
message=self.parse_response_message(e.response),
162+
status=e.response.status_code
172163
) from None
173164
except (requests.ConnectionError, requests.Timeout) as e:
174165
raise ConnectionUnavailableError(e) from None
@@ -177,7 +168,7 @@ def put(self, endpoint, body=None, params=None):
177168
"data": response.json(),
178169
"status": response.status_code
179170
}
180-
except json.decoder.JSONDecodeError:
171+
except ValueError:
181172
return {
182173
"status": response.status_code
183174
}
@@ -198,21 +189,18 @@ def delete(self, endpoint, params=None):
198189
except requests.exceptions.HTTPError as e:
199190
if e.response.status_code == 401:
200191
raise UnauthorizedError(
201-
message='The user is not authorized to make this request',
202-
status=e.response.status_code,
203-
payload=e.response
192+
message=self.parse_response_message(e.response),
193+
status=e.response.status_code
204194
) from None
205195
elif e.response.status_code == 404:
206196
raise NotFoundError(
207-
message='Resource not found',
208-
status=e.response.status_code,
209-
payload=e.response
197+
message=self.parse_response_message(e.response),
198+
status=e.response.status_code
210199
) from None
211200
else:
212201
raise HttpError(
213-
message='An HTTP error occurred',
214-
status=e.response.status_code,
215-
payload=e.response
202+
message=self.parse_response_message(e.response),
203+
status=e.response.status_code
216204
) from None
217205
except (requests.ConnectionError, requests.Timeout) as e:
218206
raise ConnectionUnavailableError(e) from None
@@ -221,7 +209,7 @@ def delete(self, endpoint, params=None):
221209
"data": response.json(),
222210
"status": response.status_code
223211
}
224-
except json.decoder.JSONDecodeError:
212+
except ValueError:
225213
return {
226214
"status": response.status_code
227215
}
@@ -239,3 +227,15 @@ def is_json(input_object):
239227
return True
240228
except ValueError:
241229
return False
230+
231+
@staticmethod
232+
def parse_response_message(response):
233+
"""Method to determine the message body from a response object. Will return None if message cannot be parsed.
234+
235+
:param response: the Requests response object
236+
:returns: the response message if parsable, otherwise None
237+
"""
238+
try:
239+
return json.loads(response.text)['message']
240+
except (ValueError, KeyError):
241+
return None

tests/test_server.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ def test_get_not_found_error(self, mock_get, mock_server_get, server_instance=se
2626
server_instance.get(endpoint='')
2727
assert context.exception.message == 'Resource not found'
2828
assert context.exception.status == 404
29-
assert context.exception.payload == 'error payload'
3029

3130
@patch('maproulette.api.maproulette_server.MapRouletteServer.get')
3231
@patch('requests.Session.get')
@@ -265,3 +264,20 @@ def test_delete_connection_error(self, mock_delete, mock_server_delete, server_i
265264
assert context.exception.message == 'Connection Unavailable'
266265
assert context.exception.status == 500
267266
assert context.exception.payload == 'error payload'
267+
268+
@patch('json.loads')
269+
def test_parse_response_message(self, mock_loads, server_instance=server):
270+
mock_loads.return_value = {
271+
'message': 'some message'
272+
}
273+
self.assertTrue(server_instance.parse_response_message(mock_loads))
274+
275+
@patch('json.loads')
276+
def test_parse_response_message_value_error(self, mock_loads, server_instance=server):
277+
mock_loads.side_effect = ValueError()
278+
self.assertIsNone(server_instance.parse_response_message(mock_loads))
279+
280+
@patch('json.loads')
281+
def test_parse_response_message_key_error(self, mock_loads, server_instance=server):
282+
mock_loads.side_effect = KeyError()
283+
self.assertIsNone(server_instance.parse_response_message(mock_loads))

0 commit comments

Comments
 (0)