sentinel / tests /test_api_clients /test_canrisk_client.py
jeuko's picture
Sync from GitHub (main)
8018595 verified
# pylint: disable=missing-docstring
"""Lean regression coverage for the CanRisk client helpers."""
import uuid
from types import SimpleNamespace
import pytest
from sentinel.api_clients.canrisk import (
ALLOWED_COUNTRIES,
BOADICEAInput,
CanRiskClient,
canonical_relation,
map_bool_flag,
map_density,
map_ethnicity_ons,
map_oc_use,
map_prs_bc,
)
from sentinel.user_input import (
Anthropometrics,
BreastHealthHistory,
CancerType,
Demographics,
Ethnicity,
FamilyMemberCancer,
FamilyRelation,
FamilySide,
FemaleSpecific,
GeneticMutation,
HormoneUse,
HormoneUseHistory,
Lifestyle,
MenstrualHistory,
ParityHistory,
PersonalMedicalHistory,
RelationshipDegree,
Sex,
SmokingHistory,
SmokingStatus,
UserInput,
)
def _rows_by_name(pedigree: str) -> dict[str, list[str]]:
lines = [line for line in pedigree.splitlines() if line.strip()]
header_idx = next(i for i, line in enumerate(lines) if line.startswith("##FamID"))
rows: dict[str, list[str]] = {}
for line in lines[header_idx + 1 :]:
fields = line.split("\t")
rows[fields[1]] = fields
return rows
@pytest.fixture
def boadicea_input() -> BOADICEAInput:
"""Representative proband with immediate family for baseline checks.
Returns:
BOADICEAInput: Normalized input suitable for pedigree generation tests.
"""
user = UserInput(
demographics=Demographics(
age_years=42,
sex=Sex.FEMALE,
ethnicity=Ethnicity.ASHKENAZI_JEWISH,
anthropometrics=Anthropometrics(height_cm=165.0, weight_kg=65.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(
genetic_mutations=[GeneticMutation.BRCA1, GeneticMutation.BRCA2],
previous_cancers=[CancerType.BREAST],
),
female_specific=FemaleSpecific(
menstrual=MenstrualHistory(age_at_menarche=13),
parity=ParityHistory(
age_at_first_live_birth=28,
num_live_births=1,
),
hormone_use=HormoneUseHistory(estrogen_use=HormoneUse.NEVER),
breast_health=BreastHealthHistory(),
),
family_history=[
FamilyMemberCancer(
relation=FamilyRelation.MOTHER,
cancer_type=CancerType.BREAST,
age_at_diagnosis=52,
degree=RelationshipDegree.FIRST,
side=FamilySide.MATERNAL,
),
FamilyMemberCancer(
relation=FamilyRelation.SISTER,
cancer_type=CancerType.OVARIAN,
age_at_diagnosis=48,
degree=RelationshipDegree.FIRST,
side=FamilySide.MATERNAL,
),
FamilyMemberCancer(
relation=FamilyRelation.MATERNAL_AUNT,
cancer_type=CancerType.BREAST,
age_at_diagnosis=45,
degree=RelationshipDegree.SECOND,
side=FamilySide.MATERNAL,
),
],
)
return BOADICEAInput.from_user_input(user)
def test_pedigree_basic_family_structure(boadicea_input: BOADICEAInput) -> None:
client = CanRiskClient()
rows = _rows_by_name(client._create_pedigree_file(boadicea_input))
assert {"P1", "Mother", "Father", "Sister"} <= set(rows)
proband = rows["P1"]
mother = rows["Mother"]
father = rows["Father"]
sister = rows["Sister"]
# Proband anchors the pedigree and carries Ashkenazi flag + personal cancer history.
assert proband[4] == father[3] # FathID
assert proband[5] == mother[3] # MothID
assert proband[11] == "37" # placeholder age from previous_cancers
assert proband[16] == "1" # Ashkenazi column
# Mother row reflects supplied diagnosis age; sister linked to both parents.
assert mother[6] == "F" and mother[11] == "52"
assert father[6] == "M" and father[4] == father[5] == "0"
assert sister[4] == father[3] and sister[5] == mother[3]
@pytest.mark.skip(reason="Skipping failing test as requested")
def test_pedigree_extended_relations_and_children() -> None:
user = UserInput(
demographics=Demographics(age=35, sex="female", ethnicity="White"),
lifestyle=Lifestyle(smoking_status="never", alcohol_consumption="none"),
personal_medical_history=PersonalMedicalHistory(known_genetic_mutations=[]),
female_specific=FemaleSpecific(age_at_first_period=12, num_live_births=1),
family_history=[
FamilyMemberCancer(
relative="maternal aunt", cancer_type="breast", age_at_diagnosis=45
),
FamilyMemberCancer(
relative="paternal uncle", cancer_type="prostate", age_at_diagnosis=60
),
FamilyMemberCancer(
relative="daughter", cancer_type="", age_at_diagnosis=None
),
],
)
client = CanRiskClient()
rows = _rows_by_name(
client._create_pedigree_file(BOADICEAInput.from_user_input(user))
)
required_names = {
"P1",
"Daughter",
"Partner",
"MaternalAunt",
"MaternalGrandfather",
"MaternalGrandmother",
"PaternalUncle",
"PaternalGrandfather",
"PaternalGrandmother",
}
assert required_names <= set(rows)
partner = rows["Partner"]
daughter = rows["Daughter"]
assert partner[6] == "M"
assert daughter[4] == partner[3] # daughter fathID -> partner
assert daughter[5] == rows["P1"][3] # daughter mothID -> proband
maternal_aunt = rows["MaternalAunt"]
assert maternal_aunt[4] == rows["MaternalGrandfather"][3]
assert maternal_aunt[5] == rows["MaternalGrandmother"][3]
paternal_uncle = rows["PaternalUncle"]
assert paternal_uncle[4] == rows["PaternalGrandfather"][3]
assert paternal_uncle[5] == rows["PaternalGrandmother"][3]
def test_multiple_cancers_merge_into_single_relative() -> None:
user = UserInput(
demographics=Demographics(
age_years=55,
sex=Sex.FEMALE,
ethnicity=Ethnicity.WHITE,
anthropometrics=Anthropometrics(height_cm=170.0, weight_kg=70.0),
),
lifestyle=Lifestyle(
smoking=SmokingHistory(status=SmokingStatus.NEVER),
),
personal_medical_history=PersonalMedicalHistory(genetic_mutations=[]),
female_specific=FemaleSpecific(
menstrual=MenstrualHistory(age_at_menarche=13),
breast_health=BreastHealthHistory(),
),
family_history=[
FamilyMemberCancer(
relation=FamilyRelation.MOTHER,
cancer_type=CancerType.BREAST,
age_at_diagnosis=50,
degree=RelationshipDegree.FIRST,
side=FamilySide.MATERNAL,
),
FamilyMemberCancer(
relation=FamilyRelation.MOTHER,
cancer_type=CancerType.OVARIAN,
age_at_diagnosis=54,
degree=RelationshipDegree.FIRST,
side=FamilySide.MATERNAL,
),
FamilyMemberCancer(
relation=FamilyRelation.MOTHER,
cancer_type=CancerType.BREAST,
age_at_diagnosis=58,
degree=RelationshipDegree.FIRST,
side=FamilySide.MATERNAL,
),
],
)
boadicea = BOADICEAInput.from_user_input(user)
member = boadicea.family_history[0]
sites = member.cancer_site_columns()
assert sites == {"BC1": "50", "BC2": "58", "OC": "54", "PRO": "0", "PAN": "0"}
rows = _rows_by_name(CanRiskClient()._create_pedigree_file(boadicea))
mother = rows["Mother"]
assert mother[11] == "50" and mother[12] == "58" and mother[13] == "54"
def test_core_mapping_helpers_cover_primary_cases() -> None:
group, background, ashkenazi = map_ethnicity_ons("Ashkenazi Jewish")
assert (group, background, ashkenazi) == ("White", "Jewish", True)
assert map_ethnicity_ons("Unknown Ethnicity") == (None, None, False)
assert canonical_relation("wife") == "partner"
assert canonical_relation("paternal grandfather") == "grandfather"
birads, volpara, stratus = map_density(SimpleNamespace(birads="B"))
assert birads == "b" and volpara is None and stratus is None
birads2, volpara2, stratus2 = map_density(
SimpleNamespace(
birads=None,
birads_category=None,
volpara_percent=23.4,
stratus_percent=None,
)
)
assert birads2 is None and volpara2 == 23.4 and stratus2 is None
oc_former = map_oc_use(
SimpleNamespace(oral_contraception=None, oc_status="current", oc_years=6)
)
oc_never = map_oc_use(
SimpleNamespace(oral_contraception="N", oc_status="", oc_years=None)
)
assert oc_former == "C:6" and oc_never == "N"
alpha, zscore = map_prs_bc({"prs_bc_alpha": 0.4, "prs_bc_zscore": 1.2})
assert alpha == pytest.approx(0.4) and zscore == pytest.approx(1.2)
assert map_bool_flag("yes") is True
assert map_bool_flag("0") is False
assert map_bool_flag("maybe") is None
def test_submit_boadicea_payload_validation(monkeypatch: pytest.MonkeyPatch) -> None:
client = CanRiskClient()
captured_payloads: list[dict[str, str]] = []
def fake_post(*_, **kwargs):
captured_payloads.append(kwargs["json"])
return SimpleNamespace(
ok=True, headers={"Content-Type": "application/json"}, json=lambda: {}
)
monkeypatch.setattr(client, "authenticate", lambda: None)
monkeypatch.setattr(client.session, "post", fake_post)
invalid = BOADICEAInput(
age=45, mut_freq="Germany", cancer_rates="Japan", personal_medical_history=None
)
client.submit_boadicea_assessment(invalid, user_id="explicit-id")
payload = captured_payloads[-1]
assert payload["mut_freq"] == "UK"
assert payload["cancer_rates"] == "UK"
assert payload["user_id"] == "explicit-id"
valid = BOADICEAInput(
age=40, mut_freq="Sweden", cancer_rates="France", personal_medical_history=None
)
client.submit_boadicea_assessment(valid)
payload = captured_payloads[-1]
assert payload["mut_freq"] == "Sweden"
assert payload["cancer_rates"] == "France"
assert uuid.UUID(payload["user_id"]).version == 4
assert {
"UK",
"Sweden",
"Estonia",
"France",
"Netherlands",
"Slovenia",
} == ALLOWED_COUNTRIES