Spaces:
Runtime error
Runtime error
| # pylint: disable=missing-docstring | |
| """Integration tests for the BOADICEA CanRisk endpoint. | |
| These tests exercise the live CanRisk service when credentials are provided | |
| via environment variables. They ensure that the pedigree fixtures we ship stay | |
| synchronised with the client implementation and that the end-to-end BOADICEA | |
| workflow returns a risk estimate within an expected range. | |
| """ | |
| import os | |
| from collections.abc import Callable | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| import pytest | |
| from sentinel.api_clients.canrisk import BOADICEAInput, CanRiskClient | |
| from sentinel.risk_models.boadicea import BOADICEARiskModel | |
| from sentinel.user_input import ( | |
| AlcoholConsumption, | |
| Anthropometrics, | |
| CancerType, | |
| Demographics, | |
| Ethnicity, | |
| FamilyMemberCancer, | |
| FamilyRelation, | |
| FamilySide, | |
| FemaleSpecific, | |
| GeneticMutation, | |
| HormoneUse, | |
| HormoneUseHistory, | |
| Lifestyle, | |
| MenstrualHistory, | |
| ParityHistory, | |
| PersonalMedicalHistory, | |
| RelationshipDegree, | |
| Sex, | |
| SmokingHistory, | |
| SmokingStatus, | |
| UserInput, | |
| ) | |
| CREDENTIALS_AVAILABLE = bool( | |
| os.getenv("CANRISK_USERNAME") and os.getenv("CANRISK_PASSWORD") | |
| ) | |
| pytestmark = pytest.mark.skipif( | |
| not CREDENTIALS_AVAILABLE, | |
| reason="CanRisk API credentials not available", | |
| ) | |
| FIXTURE_DIR = Path(__file__).resolve().parent / "fixtures" | |
| class Scenario: | |
| name: str | |
| fixture_filename: str | |
| build_user: Callable[[], UserInput] | |
| expected_range: tuple[float, float] | |
| def _high_risk_user() -> UserInput: | |
| return UserInput( | |
| demographics=Demographics( | |
| age_years=42, | |
| sex=Sex.FEMALE, | |
| ethnicity=Ethnicity.ASHKENAZI_JEWISH, | |
| anthropometrics=Anthropometrics(height_cm=165, weight_kg=65.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| alcohol_consumption=AlcoholConsumption.NONE, | |
| ), | |
| personal_medical_history=PersonalMedicalHistory( | |
| genetic_mutations=[GeneticMutation.BRCA1, GeneticMutation.BRCA2], | |
| ), | |
| 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), | |
| ), | |
| 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.UNKNOWN, | |
| ), | |
| ], | |
| ) | |
| def _moderate_risk_user() -> UserInput: | |
| return UserInput( | |
| demographics=Demographics( | |
| age_years=50, | |
| sex=Sex.FEMALE, | |
| ethnicity=Ethnicity.HISPANIC, | |
| anthropometrics=Anthropometrics(height_cm=160, weight_kg=70.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| alcohol_consumption=AlcoholConsumption.LIGHT, | |
| ), | |
| personal_medical_history=PersonalMedicalHistory( | |
| genetic_mutations=[GeneticMutation.BRCA1], | |
| ), | |
| female_specific=FemaleSpecific( | |
| menstrual=MenstrualHistory(age_at_menarche=12), | |
| parity=ParityHistory(age_at_first_live_birth=30, num_live_births=2), | |
| hormone_use=HormoneUseHistory(estrogen_use=HormoneUse.FORMER), | |
| ), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=60, | |
| degree=RelationshipDegree.FIRST, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.MATERNAL_AUNT, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=55, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.MATERNAL, | |
| ), | |
| ], | |
| ) | |
| def _average_risk_user() -> UserInput: | |
| return UserInput( | |
| demographics=Demographics( | |
| age_years=38, | |
| sex=Sex.FEMALE, | |
| ethnicity=Ethnicity.WHITE, | |
| anthropometrics=Anthropometrics(height_cm=168, weight_kg=62.0), | |
| ), | |
| lifestyle=Lifestyle( | |
| smoking=SmokingHistory(status=SmokingStatus.NEVER), | |
| alcohol_consumption=AlcoholConsumption.MODERATE, | |
| ), | |
| personal_medical_history=PersonalMedicalHistory(), | |
| female_specific=FemaleSpecific( | |
| menstrual=MenstrualHistory(age_at_menarche=12), | |
| parity=ParityHistory(num_live_births=0), | |
| hormone_use=HormoneUseHistory(estrogen_use=HormoneUse.NEVER), | |
| ), | |
| family_history=[ | |
| FamilyMemberCancer( | |
| relation=FamilyRelation.PATERNAL_GRANDMOTHER, | |
| cancer_type=CancerType.BREAST, | |
| age_at_diagnosis=67, | |
| degree=RelationshipDegree.SECOND, | |
| side=FamilySide.PATERNAL, | |
| ), | |
| ], | |
| ) | |
| SCENARIOS: tuple[Scenario, ...] = ( | |
| Scenario( | |
| name="high_risk_brca1_brca2", | |
| fixture_filename="canrisk_pedigree_high_risk.txt", | |
| build_user=_high_risk_user, | |
| expected_range=(25.0, 27.0), | |
| ), | |
| Scenario( | |
| name="moderate_risk_brca1", | |
| fixture_filename="canrisk_pedigree_moderate_risk.txt", | |
| build_user=_moderate_risk_user, | |
| expected_range=(29.0, 31.0), | |
| ), | |
| Scenario( | |
| name="average_risk_family_history", | |
| fixture_filename="canrisk_pedigree_average_risk.txt", | |
| build_user=_average_risk_user, | |
| expected_range=(2.0, 3.0), | |
| ), | |
| ) | |
| def canrisk_client() -> CanRiskClient: | |
| client = CanRiskClient() | |
| yield client | |
| client.close() | |
| def boadicea_model(canrisk_client: CanRiskClient) -> BOADICEARiskModel: | |
| return BOADICEARiskModel(client=canrisk_client) | |
| def _load_fixture_text(filename: str) -> str: | |
| path = FIXTURE_DIR / filename | |
| return path.read_text(encoding="utf-8").strip() | |
| def test_canrisk_authentication(canrisk_client: CanRiskClient) -> None: | |
| token = canrisk_client.authenticate() | |
| assert isinstance(token, str) and token, "Authentication returned an empty token" | |
| def test_boadicea_scenarios( | |
| scenario: Scenario, | |
| canrisk_client: CanRiskClient, | |
| boadicea_model: BOADICEARiskModel, | |
| ) -> None: | |
| user = scenario.build_user() | |
| boadicea_input = BOADICEAInput.from_user_input(user) | |
| pedigree = canrisk_client._create_pedigree_file(boadicea_input).strip() | |
| expected_pedigree = _load_fixture_text(scenario.fixture_filename) | |
| assert pedigree == expected_pedigree, ( | |
| "Generated pedigree diverged from saved fixture" | |
| ) | |
| score = boadicea_model.compute_score(user) | |
| assert not score.startswith("N/A"), f"BOADICEA returned error: {score}" | |
| assert score.endswith("%"), f"Unexpected score format: {score}" | |
| risk_percentage = float(score.rstrip("%")) | |
| lower, upper = scenario.expected_range | |
| assert lower <= risk_percentage <= upper, ( | |
| f"Risk {risk_percentage}% outside expected range {lower}-{upper}% for scenario {scenario.name}" | |
| ) | |