Skip to content

Commit

Permalink
Add anatom_site_special field
Browse files Browse the repository at this point in the history
  • Loading branch information
danlamanna committed Nov 22, 2024
1 parent 87cdecf commit 798f645
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 0 deletions.
9 changes: 9 additions & 0 deletions isic_metadata/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,15 @@ class AnatomSiteGeneralEnum(str, Enum):
oral_genital = "oral/genital"


class AnatomSiteSpecialEnum(str, Enum):
acral_nos = "acral NOS"
nail_nos = "nail NOS"
fingernail = "fingernail"
toenail = "toenail"
acral_palms_soles = "acral palms or soles"
oral_genital = "oral or genital"


class ColorTintEnum(str, Enum):
blue = "blue"
pink = "pink"
Expand Down
53 changes: 53 additions & 0 deletions isic_metadata/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
from isic_metadata.fields import (
Age,
AnatomSiteGeneralEnum,
AnatomSiteSpecialEnum,
BenignMalignantEnum,
ClinSizeLongDiamMm,
ColorTintEnum,
Expand Down Expand Up @@ -206,6 +207,7 @@ class MetadataRow(BaseModel):
) = None
sex: Literal["male", "female"] | None = None
anatom_site_general: AnatomSiteGeneralEnum | None = None
anatom_site_special: AnatomSiteSpecialEnum | None = None
benign_malignant: BenignMalignantEnum | None = None
diagnosis: (
Annotated[
Expand Down Expand Up @@ -442,6 +444,57 @@ def validate_dermoscopic_fields(self) -> MetadataRow:

return self

@model_validator(mode="after")
def validate_anatom_site_special(self) -> MetadataRow:
if not self.anatom_site_special:
return self

if not self.anatom_site_general:
raise error_missing_field("anatom_site_special", "anatom_site_general")

valid_combinations = {
AnatomSiteSpecialEnum.acral_nos: [
AnatomSiteGeneralEnum.upper_extremity,
AnatomSiteGeneralEnum.lower_extremity,
AnatomSiteGeneralEnum.palms_soles,
],
AnatomSiteSpecialEnum.nail_nos: [
AnatomSiteGeneralEnum.upper_extremity,
AnatomSiteGeneralEnum.lower_extremity,
AnatomSiteGeneralEnum.palms_soles,
],
AnatomSiteSpecialEnum.fingernail: [
AnatomSiteGeneralEnum.upper_extremity,
AnatomSiteGeneralEnum.palms_soles,
],
AnatomSiteSpecialEnum.toenail: [
AnatomSiteGeneralEnum.lower_extremity,
AnatomSiteGeneralEnum.palms_soles,
],
AnatomSiteSpecialEnum.acral_palms_soles: [
AnatomSiteGeneralEnum.upper_extremity,
AnatomSiteGeneralEnum.lower_extremity,
AnatomSiteGeneralEnum.palms_soles,
],
AnatomSiteSpecialEnum.oral_genital: [
AnatomSiteGeneralEnum.head_neck,
AnatomSiteGeneralEnum.oral_genital,
],
}

if self.anatom_site_special.value not in valid_combinations:
return self

if self.anatom_site_general.value not in valid_combinations[self.anatom_site_special.value]:
raise error_incompatible_fields(
"anatom_site_special",
"anatom_site_general",
self.anatom_site_special.value,
self.anatom_site_general.value,
)

return self

@model_validator(mode="after")
def validate_tbp_tile_fields(self) -> MetadataRow:
if not self.tbp_tile_type:
Expand Down
43 changes: 43 additions & 0 deletions tests/test_fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest

from isic_metadata.diagnosis_hierarchical import DiagnosisEnum
from isic_metadata.fields import AnatomSiteGeneralEnum
from isic_metadata.metadata import MetadataRow, convert_errors


Expand Down Expand Up @@ -124,3 +125,45 @@ def test_clin_size_long_diam_mm_invalid():
MetadataRow.model_validate({"clin_size_long_diam_mm": "foo"})
assert len(excinfo.value.errors()) == 1
assert "Unable to parse value as a number" in convert_errors(excinfo.value)[0]["msg"]


@pytest.mark.parametrize(
("anatom_site_special", "anatom_site_general_values"),
[
("acral NOS", ["upper extremity", "lower extremity", "palms/soles"]),
("nail NOS", ["upper extremity", "lower extremity", "palms/soles"]),
("fingernail", ["upper extremity", "palms/soles"]),
("toenail", ["lower extremity", "palms/soles"]),
("acral palms or soles", ["upper extremity", "lower extremity", "palms/soles"]),
("oral or genital", ["head/neck", "oral/genital"]),
],
)
def test_anatom_site_special(anatom_site_special: str, anatom_site_general_values: list[str]):
for anatom_site_general_value in anatom_site_general_values:
metadata = MetadataRow.model_validate(
{
"anatom_site_special": anatom_site_special,
"anatom_site_general": anatom_site_general_value,
}
)
assert metadata.anatom_site_special == anatom_site_special
assert metadata.anatom_site_general == anatom_site_general_value

for invalid_anatom_site_general in AnatomSiteGeneralEnum:
if invalid_anatom_site_general.value not in anatom_site_general_values:
with pytest.raises(ValidationError) as excinfo:
MetadataRow.model_validate(
{
"anatom_site_special": anatom_site_special,
"anatom_site_general": invalid_anatom_site_general.value,
}
)
assert len(excinfo.value.errors()) == 1
assert "is incompatible with anatom_site_general" in excinfo.value.errors()[0]["msg"]


def test_anatom_site_special_requires_anatom_site_general():
with pytest.raises(ValidationError) as excinfo:
MetadataRow.model_validate({"anatom_site_special": "acral NOS"})
assert len(excinfo.value.errors()) == 1
assert "requires setting anatom_site_general" in excinfo.value.errors()[0]["msg"]

0 comments on commit 798f645

Please sign in to comment.