Skip to content

Commit

Permalink
Merge pull request #208 from rcpch/staging
Browse files Browse the repository at this point in the history
Release #201
  • Loading branch information
mbarton authored Mar 25, 2024
2 parents 7ea9476 + bc4d426 commit d56067a
Show file tree
Hide file tree
Showing 8 changed files with 260 additions and 98 deletions.
4 changes: 3 additions & 1 deletion openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@
"utilities"
],
"summary": "Mid Parental Height Endpoint",
"description": "## Mid-parental-height Endpoint\n\n* Calculates mid-parental-height\n* Returns mid-parental centile and SDS, as well as centile lines for mid-parental height\n* and +2 SD and -SD",
"description": "## Mid-parental-height Endpoint\n\n* Calculates mid-parental-height\n* Returns mid-parental centile and SDS, as well as centile lines for mid-parental height\n* and +2 SD and -SD\n\nNote any paternal height above 253cm or maternal height above 250cm in a boy or\nany maternal height above 237 cm or any paternal height above 249 cm in a girl will return\na midparental height whose centile is 100 which generates an error when calculating a plottable centile\n(since 100% is not technically plottable)\nGiven that the tallest woman ever to live is Rumeysa Gelgi (born 1979) is 215.16 cm\nand the tallest man was 272 cm it seems reasonable to cap the upper limit for validation purposes\nto 245 cm in either parent (which is over 8 foot).\n\nif this function is called outside of the API and heights above those detailed above are higher,\nan empty array for that centile is returned. This is likely not compatible with the charting component\nas it will not be possible to render the area between the centiles if one is empty.",
"operationId": "mid_parental_height_endpoint_utilities_mid_parental_height_post",
"requestBody": {
"content": {
Expand Down Expand Up @@ -1972,12 +1972,14 @@
"properties": {
"height_paternal": {
"type": "number",
"maximum": 245.0,
"minimum": 50.0,
"title": "Height Paternal",
"description": "The height of the child's biological father, passed as float, measured in centimeters"
},
"height_maternal": {
"type": "number",
"maximum": 245.0,
"minimum": 50.0,
"title": "Height Maternal",
"description": "The height of the child's biological mother, passed as float, measured in centimeters"
Expand Down
65 changes: 43 additions & 22 deletions routers/utilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,36 @@
)


@utilities.post('/mid-parental-height', tags=['utilities'], response_model=MidParentalHeightResponse)
@utilities.post(
"/mid-parental-height", tags=["utilities"], response_model=MidParentalHeightResponse
)
def mid_parental_height_endpoint(mid_parental_height_request: MidParentalHeightRequest):
"""
## Mid-parental-height Endpoint
* Calculates mid-parental-height
* Returns mid-parental centile and SDS, as well as centile lines for mid-parental height
* and +2 SD and -SD
Note any paternal height above 253cm or maternal height above 250cm in a boy or
any maternal height above 237 cm or any paternal height above 249 cm in a girl will return
a midparental height whose centile is 100 which generates an error when calculating a plottable centile
(since 100% is not technically plottable)
Given that the tallest woman ever to live is Rumeysa Gelgi (born 1979) is 215.16 cm
and the tallest man was 272 cm it seems reasonable to cap the upper limit for validation purposes
to 245 cm in either parent (which is over 8 foot).
if this function is called outside of the API and heights above those detailed above are higher,
an empty array for that centile is returned. This is likely not compatible with the charting component
as it will not be possible to render the area between the centiles if one is empty.
"""
height = mid_parental_height(mid_parental_height_request.height_paternal,
mid_parental_height_request.height_maternal,
mid_parental_height_request.sex)


height = mid_parental_height(
mid_parental_height_request.height_paternal,
mid_parental_height_request.height_maternal,
mid_parental_height_request.sex,
)

"""
## Calculate SDS and centile
"""
Expand All @@ -44,12 +60,12 @@ def mid_parental_height_endpoint(mid_parental_height_request: MidParentalHeightR
mph_upper_centile_data = None
try:
mph_sds = sds_for_measurement(
reference=constants.UK_WHO,
age=20.0,
measurement_method=constants.HEIGHT,
observation_value=height,
sex=mid_parental_height_request.sex
)
reference=constants.UK_WHO,
age=20.0,
measurement_method=constants.HEIGHT,
observation_value=height,
sex=mid_parental_height_request.sex,
)

except Exception:
print("It was not possible to calculate midparental SDS.")
Expand All @@ -58,54 +74,59 @@ def mid_parental_height_endpoint(mid_parental_height_request: MidParentalHeightR
mph_centile = centile(mph_sds)
except:
print("It was not possible to calculate a centile from midparental height.")

try:
mph_centile_data = create_chart(
reference=UK_WHO,
centile_format=[mph_centile],
measurement_method=constants.HEIGHT,
sex=mid_parental_height_request.sex
sex=mid_parental_height_request.sex,
)
except Exception as e:
print(e)

mph_centile_data = []

try:
lower_centile = centile(mph_sds - 2)
mph_lower_centile_data = create_chart(
reference=UK_WHO,
centile_format=[lower_centile],
measurement_method=constants.HEIGHT,
sex=mid_parental_height_request.sex
sex=mid_parental_height_request.sex,
)

except Exception as e:
print(e)

mph_lower_centile_data = []

try:
upper_centile = centile(mph_sds + 2)

mph_upper_centile_data = create_chart(
reference=UK_WHO,
centile_format=[upper_centile],
measurement_method=constants.HEIGHT,
sex=mid_parental_height_request.sex
sex=mid_parental_height_request.sex,
)

except Exception as e:
print(e)
mph_upper_centile_data = []
print(f"Error: {e}")

try:
upper_height = measurement_from_sds(
reference=constants.UK_WHO,
age=20,
sex=mid_parental_height_request.sex,
measurement_method=constants.HEIGHT,
requested_sds=mph_sds + 2
requested_sds=mph_sds + 2,
)
lower_height = measurement_from_sds(
reference=constants.UK_WHO,
age=20,
sex=mid_parental_height_request.sex,
measurement_method=constants.HEIGHT,
requested_sds=mph_sds - 2
requested_sds=mph_sds - 2,
)
except Exception as e:
print(e)
Expand All @@ -118,5 +139,5 @@ def mid_parental_height_endpoint(mid_parental_height_request: MidParentalHeightR
"mid_parental_height_lower_centile_data": mph_lower_centile_data,
"mid_parental_height_upper_centile_data": mph_upper_centile_data,
"mid_parental_height_lower_value": lower_height,
"mid_parental_height_upper_value": upper_height
"mid_parental_height_upper_value": upper_height,
}
25 changes: 23 additions & 2 deletions schemas/request_validation_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@ class MeasurementRequest(BaseModel):
We aim to specify all textual information, constraints, and validation here.
It all ends up in the openAPI documentation, automagically.
"""

gestation_days: Optional[int] = Field(
0,
ge=0,
Expand Down Expand Up @@ -95,7 +94,6 @@ def birth_date_not_after_clinic_date(cls, v, info: FieldValidationInfo):
raise ValueError("Birth date cannot be after observation date.")
return v


cole_centiles = COLE_TWO_THIRDS_SDS_NINE_CENTILES
three_percent_centiles = THREE_PERCENT_CENTILES

Expand Down Expand Up @@ -216,13 +214,36 @@ class FictionalChildRequest(BaseModel):
class MidParentalHeightRequest(BaseModel):
height_paternal: float = Field(
ge=50,
le=245,
description="The height of the child's biological father, passed as float, measured in centimeters",
)
height_maternal: float = Field(
ge=50,
le=245,
description="The height of the child's biological mother, passed as float, measured in centimeters",
)
sex: Literal["male", "female"] = Field(
...,
description="The sex of the patient, as a string value which can either be `male` or `female`. Abbreviations or alternatives are not accepted.",
)


"""
the shortest man in the world was 54.6 cm Chandra Bahadur Dangi
the shortest woman in the world is Jyoti Kishanji Amge at 62.8 cm
lower limit to paternal and maternal height here therefore set at 50 cm
this will need adding to the constants in RCPCHGrowth
Upper limits are more complicated.
Note any paternal height above 253cm or maternal height above 250cm in a boy or
any maternal height above 237 cm or any paternal height above 249 cm in a girl will return
a midparental height whose centile is 100 which generates an error when calculating a plottable centile
(since 100% is not technically plottable)
Given that the tallest woman ever to live is Rumeysa Gelgi (born 1979) is 215.16 cm
and the tallest man was 272 cm it seems reasonable to cap the upper limit for validation purposes
to 245 cm in either parent (which is over 8 foot).
if this function is called outside of the API and heights above those detailed above are higher,
an empty array for that centile is returned. This is likely not compatible with the charting component
as it will not be possible to render the area between the centiles if one is empty.
"""
1 change: 1 addition & 0 deletions tests/test_data/test_midparental_height_valid.json

Large diffs are not rendered by default.

15 changes: 0 additions & 15 deletions tests/test_trisomy21.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,6 @@ def test_trisomy_21_calculation_with_invalid_request():

assert response.status_code == 422

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# restructure the response to make it easier to assert tests specifically
validation_errors = {error["loc"][1]: error for error in response.json()["detail"]}
assert (
validation_errors["birth_date"]["msg"]
Expand Down Expand Up @@ -126,8 +124,6 @@ def test_trisomy_21_chart_data_with_invalid_request():

assert response.status_code == 422

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# restructure the response to make it easier to assert tests specifically
validation_errors = {error["loc"][1]: error for error in response.json()["detail"]}
# check the validation errors are the ones we expect
assert validation_errors["sex"]["msg"] == "Input should be 'male' or 'female'"
Expand All @@ -137,13 +133,6 @@ def test_trisomy_21_chart_data_with_invalid_request():
)


"""
Fictional child data generation seems to be failing this test - Pydantic does not allow any metrics of Hours or Minutes to be passed to dates - and somewhere
is setting datetime.datetime to have a time of 12:00
"""


@pytest.mark.skip
def test_trisomy_21_fictional_child_data_with_valid_request():

body = {
Expand All @@ -167,8 +156,6 @@ def test_trisomy_21_fictional_child_data_with_valid_request():

assert response.status_code == 200

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# load the known-correct response from file
with open(
r"tests/test_data/test_trisomy_21_fictional_child_data_valid.json", "r"
) as file:
Expand Down Expand Up @@ -200,8 +187,6 @@ def test_trisomy_21_fictional_child_data_with_invalid_request():

assert response.status_code == 422

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# restructure the response to make it easier to assert tests specifically
validation_errors = {error["loc"][1]: error for error in response.json()["detail"]}
assert (
validation_errors["measurement_method"]["msg"]
Expand Down
74 changes: 39 additions & 35 deletions tests/test_turner.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,11 @@ def test_turner_calculation_with_valid_request():

assert response.status_code == 200

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# load the known-correct response from file
# with open(r'tests/test_data/test_turner_calculation_valid.json', 'r') as file:
# calculation_file = file.read()
with open(r"tests/test_data/test_turner_calculation_valid.json", "r") as file:
calculation_file = file.read()
# load the two JSON responses as Python Dicts so enable comparison (slow but more reliable)
# assert response.json() == json.loads(calculation_file)
assert response.json() == json.loads(calculation_file)


def test_turner_calculation_with_invalid_request():
Expand All @@ -57,25 +56,37 @@ def test_turner_calculation_with_invalid_request():

assert response.status_code == 422

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# restructure the response to make it easier to assert tests specifically
# validation_errors = {error['loc'][1]: error for error in response.json(
# )['detail']}
# assert validation_errors['birth_date']['msg'] == "time data 'invalid_birth_date' does not match format '%Y-%m-%d'"
# assert validation_errors['gestation_days']['msg'] == "value is not a valid integer"
# assert validation_errors['gestation_weeks']['msg'] == "value is not a valid integer"
# assert validation_errors['measurement_method']['msg'] == "unexpected value; permitted: 'height', 'weight', 'ofc', 'bmi'"
# assert validation_errors['observation_date']['msg'] == "invalid date format"
# assert validation_errors['observation_value']['msg'] == "value is not a valid float"
# assert validation_errors['sex']['msg'] == "unexpected value; permitted: 'male', 'female'"


"""
The chart-coordinates -related tests seem to be failing, complaining that at loc response, centile_data, 0, RootModel is a required field
"""
validation_errors = {error["loc"][1]: error for error in response.json()["detail"]}
assert (
validation_errors["birth_date"]["msg"]
== "Value error, time data 'invalid_birth_date' does not match format '%Y-%m-%d'"
)
assert (
validation_errors["gestation_days"]["msg"]
== "Input should be a valid integer, unable to parse string as an integer"
)
assert (
validation_errors["gestation_weeks"]["msg"]
== "Input should be a valid integer, unable to parse string as an integer"
)
assert (
validation_errors["measurement_method"]["msg"]
== "Input should be 'height', 'weight', 'ofc' or 'bmi'"
)
assert (
validation_errors["observation_date"]["msg"]
== "Input should be a valid date or datetime, invalid character in year"
)
assert (
validation_errors["observation_value"]["msg"]
== "Input should be a valid number, unable to parse string as a number"
)
assert validation_errors["sex"]["msg"] == "Input should be 'male' or 'female'"


@pytest.mark.skip
@pytest.mark.skip(
reason="chart coordinates are hashed - need a better way to test this. Unhashing takes too long."
)
def test_turner_chart_data_with_valid_request():
body = {
"measurement_method": "height",
Expand All @@ -87,7 +98,7 @@ def test_turner_chart_data_with_valid_request():

assert response.status_code == 200

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# COMMENTED OUT PENDING FIX NOT REQUIRING HASHING
# load the known-correct response from file and create a hash of it
# with open(r'tests/test_data/test_turner_female_height_valid.json', 'r') as file:
# chart_data_file = file.read()
Expand All @@ -110,7 +121,7 @@ def test_turner_chart_data_with_invalid_request():

assert response.status_code == 422

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# COMMENTED OUT PENDING FIX NOT REQUIRING HASHING
# restructure the response to make it easier to assert tests specifically
# validation_errors = {error['loc'][1]: error for error in response.json(
# )['detail']}
Expand All @@ -119,13 +130,6 @@ def test_turner_chart_data_with_invalid_request():
# assert validation_errors['measurement_method']['msg'] == "unexpected value; permitted: 'height', 'weight', 'ofc', 'bmi'"


"""
Failing for the same reason as the Trisomy21 fictional_child_data with valid request test
The generate_fictional_child_data function must be generating a time object which has Hours involved - but pydantic rejects that being passed to a Date-exclusive field
"""


@pytest.mark.skip
def test_turner_fictional_child_data_with_valid_request():

body = {
Expand All @@ -149,12 +153,13 @@ def test_turner_fictional_child_data_with_valid_request():

assert response.status_code == 200

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# load the known-correct response from file
# with open(r'tests/test_data/test_turner_fictional_child_data_valid.json', 'r') as file:
# fictional_child_data_file = file.read()
with open(
r"tests/test_data/test_turner_fictional_child_data_valid.json", "r"
) as file:
fictional_child_data_file = file.read()
# load the two JSON responses as Python Dicts so enable comparison (slow but more reliable)
# assert response.json() == json.loads(fictional_child_data_file)
assert response.json() == json.loads(fictional_child_data_file)


def test_turner_fictional_child_data_with_invalid_request():
Expand All @@ -180,7 +185,6 @@ def test_turner_fictional_child_data_with_invalid_request():

assert response.status_code == 422

# COMMENTED OUT FOR BRANCH 'dockerise' PENDING DECISION ON #166 (API Test Suite) (pacharanero, 2024-02-07 )
# restructure the response to make it easier to assert tests specifically
validation_errors = {error["loc"][1]: error for error in response.json()["detail"]}
assert (
Expand Down
Loading

0 comments on commit d56067a

Please sign in to comment.