health.qrisk-inputs-validate@1.0.1
impl/python.py
5,486 bytes · the Python implementation · view raw
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import math
from typing import List, Optional, Sequence
from .health_qrisk_inputs_validate_types import QriskInputError, QriskInputs, QriskInputsValidation
from .validation_uk_postcode import validate_uk_postcode ← from validation.uk-postcode ^2.0.0 · built alongside by fune
_SEXES = ("female", "male")
_ETHNICITIES = (
"white-or-not-stated",
"indian",
"pakistani",
"bangladeshi",
"chinese",
"other-asian",
"black-caribbean",
"black-african",
"other",
)
_SMOKING = ("non-smoker", "ex-smoker", "light", "moderate", "heavy")
_DIABETES = ("none", "type-1", "type-2")
# A value of the wrong type is a programming error, not a clinical one: it
# raises rather than being reported.
def _whole_or_none(name: str, value: object) -> Optional[int]:
if value is None:
return None
if isinstance(value, bool) or not (
isinstance(value, int) or (isinstance(value, float) and math.isfinite(value) and value.is_integer())
):
raise TypeError("%s must be a whole number or null, received %r" % (name, value))
return int(value)
def _number_or_none(name: str, value: object) -> Optional[float]:
if value is None:
return None
if isinstance(value, bool) or not isinstance(value, (int, float)) or not math.isfinite(value):
raise TypeError("%s must be a number or null, received %r" % (name, value))
return float(value)
def _text_or_none(name: str, value: object) -> Optional[str]:
if value is None:
return None
if not isinstance(value, str):
raise TypeError("%s must be text or null, received %r" % (name, value))
return value
def validate_qrisk_inputs(inputs: QriskInputs) -> QriskInputsValidation:
"""Check a QRISK3 input set against the calculator's published input
definitions (qrisk.org form and its validation). It reports every problem,
in field order, and never computes a risk."""
errors: List[QriskInputError] = []
def add(field: str, reason: str, message: str) -> None:
errors.append(QriskInputError(field=field, reason=reason, message=message))
age = _whole_or_none("age", inputs.age)
sex = _text_or_none("sex", inputs.sex)
ethnicity = _text_or_none("ethnicity", inputs.ethnicity)
postcode = _text_or_none("postcode", inputs.postcode)
smoking = _text_or_none("smoking", inputs.smoking)
diabetes = _text_or_none("diabetes", inputs.diabetes)
ratio = _number_or_none("cholesterolHdlRatio", inputs.cholesterol_hdl_ratio)
sbp = _whole_or_none("systolicBp", inputs.systolic_bp)
sbp_sd = _number_or_none("systolicBpSd", inputs.systolic_bp_sd)
height = _whole_or_none("heightCm", inputs.height_cm)
weight = _whole_or_none("weightKg", inputs.weight_kg)
age_message = "Age: enter an integer between 25 and 84"
if age is None:
add("age", "required", age_message)
elif age < 25 or age > 84:
add("age", "out-of-range", age_message)
def choice(field: str, value: Optional[str], allowed: Sequence[str], label: str) -> None:
message = "%s: enter one of %s" % (label, ", ".join(allowed))
if value is None:
add(field, "required", message)
elif value not in allowed:
add(field, "unknown-value", message)
choice("sex", sex, _SEXES, "Sex")
choice("ethnicity", ethnicity, _ETHNICITIES, "Ethnicity")
# Blank means unknown, as on the calculator ("leave blank if unknown").
if postcode is not None and postcode.strip() != "" and not validate_uk_postcode(postcode).valid:
add("postcode", "bad-postcode", "Postcode: the postcode is not recognised. Please re-enter or leave blank.")
choice("smoking", smoking, _SMOKING, "Smoking status")
choice("diabetes", diabetes, _DIABETES, "Diabetes status")
if ratio is not None and (ratio < 1.0 or ratio > 11.0):
add(
"cholesterolHdlRatio",
"out-of-range",
"Cholesterol/HDL ratio: either leave blank or enter a number between 1.0 and 11.0.",
)
if sbp is not None and (sbp < 70 or sbp > 210):
add("systolicBp", "out-of-range", "Systolic blood pressure: either leave blank or enter an integer between 70 and 210")
if sbp_sd is not None and (sbp_sd < 0.0 or sbp_sd > 40.0):
add(
"systolicBpSd",
"out-of-range",
"Standard deviation of SBP: either leave blank or enter a value between 0.0 and 40.0",
)
pair = "You have only entered one of height and weight: please enter both or leave both blank."
if height is None and weight is not None:
add("heightCm", "incomplete-pair", pair)
if height is not None and (height < 140 or height > 210):
add("heightCm", "out-of-range", "Height: enter an integer between 140 and 210")
if weight is None and height is not None:
add("weightKg", "incomplete-pair", pair)
if weight is not None and (weight < 40 or weight > 180):
add("weightKg", "out-of-range", "Weight: enter an integer between 40 and 180")
if inputs.existing_cvd is True:
add(
"existingCvd",
"not-applicable",
"QRISK3 is only valid for people without a diagnosis of coronary heart disease "
"(including angina or heart attack) or stroke/transient ischaemic attack.",
)
if inputs.on_statins is True:
add("onStatins", "not-applicable", "QRISK3 is only valid for people who are not on statins.")
return QriskInputsValidation(valid=len(errors) == 0, errors=errors)