-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[ENH] Support partial term fetching failures (#65)
* add test for partial node failure in terms fetching * assert over full mocked partial success attribute response * add response model for combined attribute instances * test when terms fetching fails for all nodes * handle node errors when fetching terms and do so async * remove duplicate test * refactor out mocked httpx.get raising a ConnectError * refactor out combined response processing + use generic summary console message * update comments Co-authored-by: Sebastian Urchs <[email protected]> * improve test logic * refactor func for building combined response from nodes * set status code in path operation function * refactor setting of test federation nodes into fixture * more informative docstrings and comments --------- Co-authored-by: Sebastian Urchs <[email protected]>
- Loading branch information
Showing
7 changed files
with
242 additions
and
103 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,15 +1,26 @@ | ||
from fastapi import APIRouter | ||
from fastapi import APIRouter, Response, status | ||
from pydantic import constr | ||
|
||
from .. import crud | ||
from ..models import CONTROLLED_TERM_REGEX | ||
from ..models import CONTROLLED_TERM_REGEX, CombinedAttributeResponse | ||
|
||
router = APIRouter(prefix="/attributes", tags=["attributes"]) | ||
|
||
|
||
@router.get("/{data_element_URI}") | ||
async def get_terms(data_element_URI: constr(regex=CONTROLLED_TERM_REGEX)): | ||
# We use the Response parameter below to change the status code of the response while still being able to validate the returned data using the response model. | ||
# (see https://fastapi.tiangolo.com/advanced/response-change-status-code/ for more info). | ||
# | ||
# TODO: if our response model for fully successful vs. not fully successful responses grows more complex in the future, | ||
# consider additionally using https://fastapi.tiangolo.com/advanced/additional-responses/#additional-response-with-model to document | ||
# example responses for different status codes in the OpenAPI docs (less relevant for now since there is only one response model). | ||
@router.get("/{data_element_URI}", response_model=CombinedAttributeResponse) | ||
async def get_terms( | ||
data_element_URI: constr(regex=CONTROLLED_TERM_REGEX), response: Response | ||
): | ||
"""When a GET request is sent, return a list dicts with the only key corresponding to controlled term of a neurobagel class and value corresponding to all the available terms.""" | ||
response = await crud.get_terms(data_element_URI) | ||
response_dict = await crud.get_terms(data_element_URI) | ||
|
||
return response | ||
if response_dict["errors"]: | ||
response.status_code = status.HTTP_207_MULTI_STATUS | ||
|
||
return response_dict |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,37 @@ | ||
import httpx | ||
import pytest | ||
from starlette.testclient import TestClient | ||
|
||
from app.api import utility as util | ||
from app.main import app | ||
|
||
|
||
@pytest.fixture(scope="module") | ||
def test_app(): | ||
client = TestClient(app) | ||
yield client | ||
|
||
|
||
@pytest.fixture(scope="function") | ||
def set_valid_test_federation_nodes(monkeypatch): | ||
"""Set two correctly formatted federation nodes for a test function (mocks the result of reading/parsing available public and local nodes on startup).""" | ||
monkeypatch.setattr( | ||
util, | ||
"FEDERATION_NODES", | ||
{ | ||
"https://firstpublicnode.org/": "First Public Node", | ||
"https://secondpublicnode.org/": "Second Public Node", | ||
}, | ||
) | ||
|
||
|
||
@pytest.fixture() | ||
def mock_failed_connection_httpx_get(): | ||
"""Return a mock for the httpx.AsyncClient.get method that raises a ConnectError when called.""" | ||
|
||
async def _mock_httpx_get_with_connect_error(self, **kwargs): | ||
# The self parameter is necessary to match the signature of the method being mocked, | ||
# which is a class method of the httpx.AsyncClient class (see https://www.python-httpx.org/api/#asyncclient). | ||
raise httpx.ConnectError("Some connection error") | ||
|
||
return _mock_httpx_get_with_connect_error |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
import httpx | ||
import pytest | ||
from fastapi import status | ||
|
||
|
||
def test_partially_failed_terms_fetching_handled_gracefully( | ||
test_app, | ||
monkeypatch, | ||
set_valid_test_federation_nodes, | ||
): | ||
""" | ||
When some nodes fail while getting term instances for an attribute (/attribute/{data_element_URI}), | ||
the overall API get request still succeeds, and the response includes a list of the encountered errors along with the successfully fetched terms. | ||
""" | ||
mocked_node_attribute_response = { | ||
"nb:Assessment": [ | ||
{ | ||
"TermURL": "cogatlas:trm_56a9137d9dce1", | ||
"Label": "behavioral approach/inhibition systems", | ||
}, | ||
{ | ||
"TermURL": "cogatlas:trm_55a6a8e81b7f4", | ||
"Label": "Barratt Impulsiveness Scale", | ||
}, | ||
] | ||
} | ||
|
||
async def mock_httpx_get(self, **kwargs): | ||
# The self parameter is necessary to match the signature of the method being mocked, | ||
# which is a class method of the httpx.AsyncClient class (see https://www.python-httpx.org/api/#asyncclient). | ||
if ( | ||
kwargs["url"] | ||
== "https://secondpublicnode.org/attributes/nb:Assessment" | ||
): | ||
return httpx.Response( | ||
status_code=500, json={}, text="Some internal server error" | ||
) | ||
return httpx.Response( | ||
status_code=200, | ||
json=mocked_node_attribute_response, | ||
) | ||
|
||
monkeypatch.setattr(httpx.AsyncClient, "get", mock_httpx_get) | ||
|
||
with pytest.warns(UserWarning): | ||
response = test_app.get("/attributes/nb:Assessment") | ||
|
||
assert response.status_code == status.HTTP_207_MULTI_STATUS | ||
|
||
response_object = response.json() | ||
assert response_object["errors"] == [ | ||
{ | ||
"node_name": "Second Public Node", | ||
"error": "Internal Server Error: Some internal server error", | ||
} | ||
] | ||
assert response_object["responses"] == mocked_node_attribute_response | ||
assert response_object["nodes_response_status"] == "partial success" | ||
|
||
|
||
def test_fully_failed_terms_fetching_handled_gracefully( | ||
test_app, | ||
monkeypatch, | ||
mock_failed_connection_httpx_get, | ||
set_valid_test_federation_nodes, | ||
): | ||
""" | ||
When *all* nodes fail while getting term instances for an attribute (/attribute/{data_element_URI}), | ||
the overall API get request still succeeds, but includes an overall failure status and all encountered errors in the response. | ||
""" | ||
monkeypatch.setattr( | ||
httpx.AsyncClient, "get", mock_failed_connection_httpx_get | ||
) | ||
|
||
with pytest.warns(UserWarning): | ||
response = test_app.get("/attributes/nb:Assessment") | ||
|
||
assert response.status_code == status.HTTP_207_MULTI_STATUS | ||
|
||
response = response.json() | ||
assert response["nodes_response_status"] == "fail" | ||
assert len(response["errors"]) == 2 | ||
assert response["responses"] == {"nb:Assessment": []} |
Oops, something went wrong.