Skip to content

Commit 436ed59

Browse files
committed
[lightapi/python]: change BasicConnector to allow 404 to be optionally treated as errors
problem: BasicConnector always treats 404 as success, requiring caller to perform extra validation with 404 is undesirable solution: treat 404 as error by default and add flag to optionally treat it as success
1 parent 871b433 commit 436ed59

5 files changed

Lines changed: 101 additions & 31 deletions

File tree

lightapi/python/symbollightapi/connector/BasicConnector.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def __init__(self, endpoint):
1515
self.endpoint = endpoint
1616
self.timeout_seconds = None
1717

18-
async def _dispatch(self, action, url_path, property_name, **kwargs):
18+
async def _dispatch(self, action, url_path, property_name, not_found_as_error, **kwargs):
1919
try:
2020
timeout = ClientTimeout(total=self.timeout_seconds)
2121
async with ClientSession(timeout=timeout) as session:
@@ -25,7 +25,7 @@ async def _dispatch(self, action, url_path, property_name, **kwargs):
2525
except (client_exceptions.ContentTypeError, json.decoder.JSONDecodeError) as ex:
2626
raise NodeException from ex
2727

28-
if 400 <= response.status and 404 != response.status:
28+
if 400 <= response.status and (404 != response.status or not_found_as_error):
2929
error_message = f'HTTP request failed with code {response.status}'
3030
for key in ('code', 'message'):
3131
if key in response_json:
@@ -37,30 +37,30 @@ async def _dispatch(self, action, url_path, property_name, **kwargs):
3737
except (asyncio.TimeoutError, client_exceptions.ClientConnectorError) as ex:
3838
raise NodeException from ex
3939

40-
async def get(self, url_path, property_name=None):
40+
async def get(self, url_path, property_name=None, not_found_as_error=True):
4141
"""
4242
Initiates a GET to the specified path and returns the desired property.
4343
Raises NodeException on connection or content failure.
4444
"""
4545

46-
return await self._dispatch('get', url_path, property_name)
46+
return await self._dispatch('get', url_path, property_name, not_found_as_error)
4747

48-
async def post(self, url_path, request_payload, property_name=None):
48+
async def post(self, url_path, request_payload, property_name=None, not_found_as_error=True):
4949
"""
5050
Initiates a POST to the specified path and returns the desired property.
5151
Raises NodeException on connection or content failure.
5252
"""
5353

54-
return await self._dispatch('post', url_path, property_name, data=json.dumps(request_payload), headers={
54+
return await self._dispatch('post', url_path, property_name, not_found_as_error, data=json.dumps(request_payload), headers={
5555
'Content-Type': 'application/json'
5656
})
5757

58-
async def put(self, url_path, request_payload, property_name=None):
58+
async def put(self, url_path, request_payload, property_name=None, not_found_as_error=True):
5959
"""
6060
Initiates a PUT to the specified path and returns the desired property.
6161
Raises NodeException on connection or content failure.
6262
"""
6363

64-
return await self._dispatch('put', url_path, property_name, data=json.dumps(request_payload), headers={
64+
return await self._dispatch('put', url_path, property_name, not_found_as_error, data=json.dumps(request_payload), headers={
6565
'Content-Type': 'application/json'
6666
})

lightapi/python/symbollightapi/connector/NemConnector.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,9 +80,6 @@ async def block_headers(self, height):
8080

8181
url_path = 'block/at/public'
8282
block = await self.post(url_path, {'height': height})
83-
if 'transactions' not in block:
84-
raise RuntimeError(f'node returned invalid data: {block}')
85-
8683
del block['transactions']
8784
return block
8885

lightapi/python/symbollightapi/connector/SymbolConnector.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ async def peers(self):
144144
async def account_links(self, account_id):
145145
"""Gets account links for a specified account."""
146146

147-
json_response = await self.get(f'accounts/{account_id}')
147+
json_response = await self.get(f'accounts/{account_id}', not_found_as_error=False)
148148
if _is_not_found(json_response):
149149
return LinkedPublicKeys()
150150

@@ -177,7 +177,7 @@ def _parse_links(json_supplemental_public_keys):
177177
async def account_multisig(self, address):
178178
"""Gets multisig information about an account."""
179179

180-
json_response = await self.get(f'account/{address}/multisig')
180+
json_response = await self.get(f'account/{address}/multisig', not_found_as_error=False)
181181
if _is_not_found(json_response):
182182
return MultisigInfo(0, 0, [], [])
183183

lightapi/python/tests/connector/test_BasicConnector.py

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,24 @@ async def echo_put(self, request):
3232

3333
async def status_code(self, request):
3434
status_code = int(request.match_info['status_code'])
35+
return await self._handle_status_code_request(request, status_code, 'get')
3536

37+
async def status_code_post(self, request):
38+
request_json = await request.json()
39+
status_code = request_json['status_code']
40+
return await self._handle_status_code_request(request, status_code, 'post')
41+
42+
async def status_code_put(self, request):
43+
request_json = await request.json()
44+
status_code = request_json['status_code']
45+
return await self._handle_status_code_request(request, status_code, 'put')
46+
47+
async def _handle_status_code_request(self, request, status_code, action):
3648
response_json = {
3749
'code': 'SomeCode',
3850
'message': 'some message',
39-
'status': status_code
51+
'status': status_code,
52+
'action': action
4053
}
4154

4255
if self.omit_error_description:
@@ -69,6 +82,8 @@ async def _process(self, request, response_body, status_code=200):
6982
app.router.add_post('/echo/post', mock_server.echo_post)
7083
app.router.add_put('/echo/put', mock_server.echo_put)
7184
app.router.add_get(r'/status/{status_code}', mock_server.status_code)
85+
app.router.add_post(r'/status', mock_server.status_code_post)
86+
app.router.add_put(r'/status', mock_server.status_code_put)
7287
server = await aiohttp_client(app) # pylint: disable=redefined-outer-name
7388

7489
server.mock = mock_server
@@ -250,7 +265,7 @@ async def test_can_handle_stopped_node_put():
250265
# endregion
251266

252267

253-
# region error handling - HTTP error code
268+
# region error handling - HTTP error code (general)
254269

255270
async def _assert_can_propagate_status_code_result(server, status_code): # pylint: disable=redefined-outer-name
256271
# Arrange:
@@ -260,7 +275,7 @@ async def _assert_can_propagate_status_code_result(server, status_code): # pyli
260275
response_json = await connector.get(f'status/{status_code}')
261276

262277
# Assert:
263-
assert {'code': 'SomeCode', 'message': 'some message', 'status': status_code} == response_json
278+
assert {'code': 'SomeCode', 'message': 'some message', 'status': status_code, 'action': 'get'} == response_json
264279

265280

266281
async def _assert_can_propagate_status_code_failure_result(server, status_code): # pylint: disable=redefined-outer-name
@@ -284,12 +299,12 @@ async def _assert_can_propagate_status_code_failure_result_with_message(server,
284299

285300

286301
async def test_can_propagate_http_success_results(server): # pylint: disable=redefined-outer-name
287-
for status_code in (200, 202, 300, 404):
302+
for status_code in (200, 202, 300):
288303
await _assert_can_propagate_status_code_result(server, status_code)
289304

290305

291306
async def test_can_propagate_http_failure_results(server): # pylint: disable=redefined-outer-name
292-
for status_code in (400, 401, 500, 501):
307+
for status_code in (400, 401, 404, 500, 501):
293308
await _assert_can_propagate_status_code_failure_result(server, status_code)
294309

295310

@@ -298,3 +313,74 @@ async def test_can_propagate_http_failure_results_with_message(server): # pylin
298313
await _assert_can_propagate_status_code_failure_result_with_message(server, status_code)
299314

300315
# endregion
316+
317+
318+
# region error handling - HTTP error code (404)
319+
320+
async def _assert_can_handle_http_failure_404_as_error_by_default(server, action, url_path, **kwargs):
321+
# pylint: disable=redefined-outer-name
322+
# Arrange:
323+
connector = BasicConnector(server.make_url(''))
324+
325+
# Act + Assert:
326+
with pytest.raises(NodeException, match='HTTP request failed with code 404\nSomeCode\nsome message'):
327+
await getattr(connector, action)(url_path, **kwargs)
328+
329+
330+
async def _assert_can_handle_http_failure_404_as_explicit_error(server, action, url_path, **kwargs): # pylint: disable=redefined-outer-name
331+
# Arrange:
332+
connector = BasicConnector(server.make_url(''))
333+
334+
# Act + Assert:
335+
with pytest.raises(NodeException, match='HTTP request failed with code 404\nSomeCode\nsome message'):
336+
await getattr(connector, action)(url_path, not_found_as_error=True, **kwargs)
337+
338+
339+
async def _assert_can_handle_http_failure_404_as_explicit_non_error(server, action, url_path, **kwargs):
340+
# pylint: disable=redefined-outer-name
341+
# Arrange:
342+
connector = BasicConnector(server.make_url(''))
343+
344+
# Act:
345+
response_json = await getattr(connector, action)(url_path, not_found_as_error=False, **kwargs)
346+
347+
# Assert:
348+
assert {'code': 'SomeCode', 'message': 'some message', 'status': 404, 'action': action} == response_json
349+
350+
351+
async def test_can_handle_http_failure_404_as_error_by_default_get(server): # pylint: disable=redefined-outer-name
352+
await _assert_can_handle_http_failure_404_as_error_by_default(server, 'get', 'status/404')
353+
354+
355+
async def test_can_handle_http_failure_404_as_error_by_default_post(server): # pylint: disable=redefined-outer-name
356+
await _assert_can_handle_http_failure_404_as_error_by_default(server, 'post', 'status', request_payload={'status_code': 404})
357+
358+
359+
async def test_can_handle_http_failure_404_as_error_by_default_put(server): # pylint: disable=redefined-outer-name
360+
await _assert_can_handle_http_failure_404_as_error_by_default(server, 'put', 'status', request_payload={'status_code': 404})
361+
362+
363+
async def test_can_handle_http_failure_404_as_explicit_error_get(server): # pylint: disable=redefined-outer-name
364+
await _assert_can_handle_http_failure_404_as_explicit_error(server, 'get', 'status/404')
365+
366+
367+
async def test_can_handle_http_failure_404_as_explicit_error_post(server): # pylint: disable=redefined-outer-name
368+
await _assert_can_handle_http_failure_404_as_explicit_error(server, 'post', 'status', request_payload={'status_code': 404})
369+
370+
371+
async def test_can_handle_http_failure_404_as_explicit_error_put(server): # pylint: disable=redefined-outer-name
372+
await _assert_can_handle_http_failure_404_as_explicit_error(server, 'put', 'status', request_payload={'status_code': 404})
373+
374+
375+
async def test_can_handle_http_failure_404_as_explicit_non_error_get(server): # pylint: disable=redefined-outer-name
376+
await _assert_can_handle_http_failure_404_as_explicit_non_error(server, 'get', 'status/404')
377+
378+
379+
async def test_can_handle_http_failure_404_as_explicit_non_error_post(server): # pylint: disable=redefined-outer-name
380+
await _assert_can_handle_http_failure_404_as_explicit_non_error(server, 'post', 'status', request_payload={'status_code': 404})
381+
382+
383+
async def test_can_handle_http_failure_404_as_explicit_non_error_put(server): # pylint: disable=redefined-outer-name
384+
await _assert_can_handle_http_failure_404_as_explicit_non_error(server, 'put', 'status', request_payload={'status_code': 404})
385+
386+
# endregion

lightapi/python/tests/connector/test_NemConnector.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -202,10 +202,6 @@ async def block_at(self, request):
202202
'transactions': []
203203
}
204204

205-
# make returned data invalid
206-
if 0xDEAD == request_json['height']:
207-
del block['transactions']
208-
209205
return await self._process(request, block)
210206

211207
async def node_info(self, request):
@@ -349,15 +345,6 @@ async def test_can_get_block_headers(server): # pylint: disable=redefined-outer
349345
assert {'type': 1, 'signer': PUBLIC_KEYS[0], 'timeStamp': 201000} == headers
350346
assert [f'{server.make_url("")}/block/at/public'] == server.mock.urls
351347

352-
353-
async def test_cannot_get_block_headers_when_server_returns_invalid_data(server): # pylint: disable=redefined-outer-name
354-
# Arrange:
355-
connector = NemConnector(server.make_url(''))
356-
357-
# Act + Assert:
358-
with pytest.raises(RuntimeError):
359-
await connector.block_headers(0xDEAD)
360-
361348
# endregion
362349

363350

0 commit comments

Comments
 (0)