Skip to content

Commit 85004c1

Browse files
authored
Merge pull request #62 from osmlab/dev
Merging dev -> master
2 parents b2de6ac + 1bac9c8 commit 85004c1

File tree

9 files changed

+135
-54
lines changed

9 files changed

+135
-54
lines changed

examples/challenge_examples.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,4 @@
5858
data = json.loads(data_file.read())
5959

6060
# Printing response:
61-
print(api.add_tasks_to_challenge(data, challenge_id))
61+
print(json.dumps(api.add_tasks_to_challenge(data, challenge_id)))

maproulette/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,4 @@
1414
from .api.task import Task
1515
from .api.user import User
1616

17-
__version__ = '1.3.0'
17+
__version__ = '1.4.0'

maproulette/api/challenge.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -250,7 +250,7 @@ def reset_task_instructions(self, challenge_id):
250250
def add_tasks_to_challenge(self, data, challenge_id):
251251
"""Method to add tasks to an existing challenge
252252
253-
:param data: a geojson containing geometry of tasks to be added to a challenge
253+
:param data: a GeoJSON containing geometry of tasks to be added to a challenge
254254
:param challenge_id: the ID corresponding to the challenge that tasks will be added to
255255
:returns: the API response from the PUT request
256256
"""

maproulette/api/errors.py

+34-3
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

+41-40
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ def __check_health(self, retries=3, delay=5):
4343
return True
4444
except requests.exceptions.ConnectionError:
4545
print(f"Connection not available. Attempt {str(i+1)} out of {str(retries)}")
46+
time.sleep(delay)
4647

4748
raise ConnectionUnavailableError(
4849
message='Specified server unavailable'
@@ -64,28 +65,25 @@ def get(self, endpoint, params=None):
6465
except requests.exceptions.HTTPError as e:
6566
if e.response.status_code == 404:
6667
raise NotFoundError(
67-
message='Resource not found',
68-
status=e.response.status_code,
69-
payload=e.response
68+
message=self.parse_response_message(e.response),
69+
status=e.response.status_code
7070
) from None
7171
else:
7272
raise HttpError(
73-
message='An HTTP error occurred',
74-
status=e.response.status_code,
75-
payload=e.response
73+
message=self.parse_response_message(e.response),
74+
status=e.response.status_code
7675
) from None
7776
except (requests.ConnectionError, requests.Timeout) as e:
7877
raise ConnectionUnavailableError(
79-
message="Connection Unavailable",
80-
status=e.response.status_code,
81-
payload=response
78+
message=self.parse_response_message(e.response),
79+
status=e.response.status_code
8280
) from None
8381
try:
8482
return {
8583
"data": response.json(),
8684
"status": response.status_code
8785
}
88-
except json.decoder.JSONDecodeError:
86+
except ValueError:
8987
return {
9088
"status": response.status_code
9189
}
@@ -108,21 +106,18 @@ def post(self, endpoint, body=None, params=None):
108106
except requests.exceptions.HTTPError as e:
109107
if e.response.status_code == 400:
110108
raise InvalidJsonError(
111-
message='Invalid JSON payload',
112-
status=e.response.status_code,
113-
payload=e.response
109+
message=self.parse_response_message(e.response),
110+
status=e.response.status_code
114111
) from None
115112
elif e.response.status_code == 401:
116113
raise UnauthorizedError(
117-
message='The user is not authorized to make this request',
118-
status=e.response.status_code,
119-
payload=e.response
114+
message=self.parse_response_message(e.response),
115+
status=e.response.status_code
120116
) from None
121117
else:
122118
raise HttpError(
123-
message='An HTTP error occurred',
124-
status=e.response.status_code,
125-
payload=e.response
119+
message=self.parse_response_message(e.response),
120+
status=e.response.status_code
126121
) from None
127122
except (requests.ConnectionError, requests.Timeout) as e:
128123
raise ConnectionUnavailableError(e) from None
@@ -131,7 +126,7 @@ def post(self, endpoint, body=None, params=None):
131126
"data": response.json(),
132127
"status": response.status_code
133128
}
134-
except json.decoder.JSONDecodeError:
129+
except ValueError:
135130
return {
136131
"status": response.status_code
137132
}
@@ -154,21 +149,18 @@ def put(self, endpoint, body=None, params=None):
154149
except requests.exceptions.HTTPError as e:
155150
if e.response.status_code == 400:
156151
raise InvalidJsonError(
157-
message='Invalid JSON payload',
158-
status=e.response.status_code,
159-
payload=e.response
152+
message=self.parse_response_message(e.response),
153+
status=e.response.status_code
160154
) from None
161155
elif e.response.status_code == 401:
162156
raise UnauthorizedError(
163-
message='The user is not authorized to make this request',
164-
status=e.response.status_code,
165-
payload=e.response
157+
message=self.parse_response_message(e.response),
158+
status=e.response.status_code
166159
) from None
167160
else:
168161
raise HttpError(
169-
message='An HTTP error occurred',
170-
status=e.response.status_code,
171-
payload=e.response
162+
message=self.parse_response_message(e.response),
163+
status=e.response.status_code
172164
) from None
173165
except (requests.ConnectionError, requests.Timeout) as e:
174166
raise ConnectionUnavailableError(e) from None
@@ -177,7 +169,7 @@ def put(self, endpoint, body=None, params=None):
177169
"data": response.json(),
178170
"status": response.status_code
179171
}
180-
except json.decoder.JSONDecodeError:
172+
except ValueError:
181173
return {
182174
"status": response.status_code
183175
}
@@ -198,21 +190,18 @@ def delete(self, endpoint, params=None):
198190
except requests.exceptions.HTTPError as e:
199191
if e.response.status_code == 401:
200192
raise UnauthorizedError(
201-
message='The user is not authorized to make this request',
202-
status=e.response.status_code,
203-
payload=e.response
193+
message=self.parse_response_message(e.response),
194+
status=e.response.status_code
204195
) from None
205196
elif e.response.status_code == 404:
206197
raise NotFoundError(
207-
message='Resource not found',
208-
status=e.response.status_code,
209-
payload=e.response
198+
message=self.parse_response_message(e.response),
199+
status=e.response.status_code
210200
) from None
211201
else:
212202
raise HttpError(
213-
message='An HTTP error occurred',
214-
status=e.response.status_code,
215-
payload=e.response
203+
message=self.parse_response_message(e.response),
204+
status=e.response.status_code
216205
) from None
217206
except (requests.ConnectionError, requests.Timeout) as e:
218207
raise ConnectionUnavailableError(e) from None
@@ -221,7 +210,7 @@ def delete(self, endpoint, params=None):
221210
"data": response.json(),
222211
"status": response.status_code
223212
}
224-
except json.decoder.JSONDecodeError:
213+
except ValueError:
225214
return {
226215
"status": response.status_code
227216
}
@@ -239,3 +228,15 @@ def is_json(input_object):
239228
return True
240229
except ValueError:
241230
return False
231+
232+
@staticmethod
233+
def parse_response_message(response):
234+
"""Method to determine the message body from a response object. Will return None if message cannot be parsed.
235+
236+
:param response: the Requests response object
237+
:returns: the response message if parsable, otherwise None
238+
"""
239+
try:
240+
return json.loads(response.text)['message']
241+
except (ValueError, KeyError):
242+
return None

maproulette/api/task.py

+22-5
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,20 @@ def get_task_history(self, task_id):
3030
endpoint=f"/task/{task_id}/history")
3131
return response
3232

33-
def create_tasks(self, data):
34-
"""Method to create a batch of tasks
33+
def create_tasks(self, data, batch_size=5000):
34+
"""Method to create a batch of tasks using the specified batch_size.
3535
3636
:param data: a JSON input containing task details
37+
:param batch_size: the number of tasks to post per API call. The default is 5000.
38+
:type batch_size: int
3739
:returns: the API response from the POST request
3840
"""
39-
response = self.post(
40-
endpoint="/tasks",
41-
body=data)
41+
response = []
42+
for batch in self.batch_generator(input_list=data, chunk_size=batch_size):
43+
response.append(self.post(
44+
endpoint="/tasks",
45+
body=batch)
46+
)
4247
return response
4348

4449
def update_tasks(self, data):
@@ -157,3 +162,15 @@ def is_task_model(input_object):
157162
:returns: True if instance of model
158163
"""
159164
return bool(isinstance(input_object, TaskModel))
165+
166+
@staticmethod
167+
def batch_generator(input_list, chunk_size):
168+
"""Method to yield successive n-sized chunks from input_list
169+
170+
:param input_list: the list to break into chunks
171+
:param chunk_size: the number of list items to include per chunk
172+
:type chunk_size: int
173+
:returns: an iterator for the n-sized chunks of the input_list
174+
"""
175+
for i in range(0, len(input_list), chunk_size):
176+
yield input_list[i:i + chunk_size]

maproulette/models/task.py

+1
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ def to_dict(self):
190190
"id": self._id,
191191
"name": self._name,
192192
"parent": self._parent,
193+
"geometries": self._geometries,
193194
"instruction": self._instruction,
194195
"location": self._location,
195196
"suggestedFix": self._suggested_fix,

tests/test_server.py

+17-1
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))

tests/test_task_api.py

+17-2
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,16 @@ def test_get_task_history(self, mock_request, api_instance=api):
2626

2727
@patch('maproulette.api.maproulette_server.requests.Session.post')
2828
def test_create_tasks(self, mock_request, api_instance=api):
29+
test_tasks = []
2930
geometries = test_geojson['features'][0]['geometry']
3031
test_task_model = maproulette.TaskModel(name='test_task',
3132
parent='12345',
3233
geometries=geometries)
34+
test_tasks.append(test_task_model.to_dict())
3335
mock_request.return_value.status_code = '200'
34-
response = api_instance.create_tasks(test_task_model)
35-
self.assertEqual(response['status'], '200')
36+
responses = api_instance.create_tasks(test_tasks)
37+
for response in responses:
38+
self.assertEqual(response['status'], '200')
3639

3740
@patch('maproulette.api.maproulette_server.requests.Session.put')
3841
def test_update_tasks(self, mock_request, api_instance=api):
@@ -88,3 +91,15 @@ def test_get_task_comments(self, mock_request, api_instance=api):
8891
mock_request.return_value.status_code = '200'
8992
response = api_instance.get_task_comments(task_id)
9093
self.assertEqual(response['status'], '200')
94+
95+
def test_batch_generator(self, api_instance=api):
96+
97+
batch_size = 10
98+
test_length = 1234
99+
test_list = [i for i in range(test_length)]
100+
running_total = 0
101+
for chunk in api_instance.batch_generator(test_list, batch_size):
102+
running_total += len(chunk)
103+
self.assertIsInstance(chunk, list)
104+
self.assertLessEqual(len(chunk), batch_size)
105+
self.assertEqual(test_length, running_total)

0 commit comments

Comments
 (0)