Skip to content

Commit 56ea98e

Browse files
authored
Merge pull request #44 from ImageMarkup/add-anatom-site-special
2 parents 2c8495a + 846533d commit 56ea98e

File tree

3 files changed

+105
-0
lines changed

3 files changed

+105
-0
lines changed

isic_metadata/fields.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,15 @@ class AnatomSiteGeneralEnum(str, Enum):
191191
oral_genital = "oral/genital"
192192

193193

194+
class AnatomSiteSpecialEnum(str, Enum):
195+
acral_nos = "acral NOS"
196+
nail_nos = "nail NOS"
197+
fingernail = "fingernail"
198+
toenail = "toenail"
199+
acral_palms_soles = "acral palms or soles"
200+
oral_genital = "oral or genital"
201+
202+
194203
class ColorTintEnum(str, Enum):
195204
blue = "blue"
196205
pink = "pink"

isic_metadata/metadata.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from isic_metadata.fields import (
2222
Age,
2323
AnatomSiteGeneralEnum,
24+
AnatomSiteSpecialEnum,
2425
BenignMalignantEnum,
2526
ClinSizeLongDiamMm,
2627
ColorTintEnum,
@@ -206,6 +207,7 @@ class MetadataRow(BaseModel):
206207
) = None
207208
sex: Literal["male", "female"] | None = None
208209
anatom_site_general: AnatomSiteGeneralEnum | None = None
210+
anatom_site_special: AnatomSiteSpecialEnum | None = None
209211
benign_malignant: BenignMalignantEnum | None = None
210212
diagnosis: (
211213
Annotated[
@@ -442,6 +444,57 @@ def validate_dermoscopic_fields(self) -> MetadataRow:
442444

443445
return self
444446

447+
@model_validator(mode="after")
448+
def validate_anatom_site_special(self) -> MetadataRow:
449+
if not self.anatom_site_special:
450+
return self
451+
452+
if not self.anatom_site_general:
453+
raise error_missing_field("anatom_site_special", "anatom_site_general")
454+
455+
valid_combinations = {
456+
AnatomSiteSpecialEnum.acral_nos: [
457+
AnatomSiteGeneralEnum.upper_extremity,
458+
AnatomSiteGeneralEnum.lower_extremity,
459+
AnatomSiteGeneralEnum.palms_soles,
460+
],
461+
AnatomSiteSpecialEnum.nail_nos: [
462+
AnatomSiteGeneralEnum.upper_extremity,
463+
AnatomSiteGeneralEnum.lower_extremity,
464+
AnatomSiteGeneralEnum.palms_soles,
465+
],
466+
AnatomSiteSpecialEnum.fingernail: [
467+
AnatomSiteGeneralEnum.upper_extremity,
468+
AnatomSiteGeneralEnum.palms_soles,
469+
],
470+
AnatomSiteSpecialEnum.toenail: [
471+
AnatomSiteGeneralEnum.lower_extremity,
472+
AnatomSiteGeneralEnum.palms_soles,
473+
],
474+
AnatomSiteSpecialEnum.acral_palms_soles: [
475+
AnatomSiteGeneralEnum.upper_extremity,
476+
AnatomSiteGeneralEnum.lower_extremity,
477+
AnatomSiteGeneralEnum.palms_soles,
478+
],
479+
AnatomSiteSpecialEnum.oral_genital: [
480+
AnatomSiteGeneralEnum.head_neck,
481+
AnatomSiteGeneralEnum.oral_genital,
482+
],
483+
}
484+
485+
if self.anatom_site_special.value not in valid_combinations:
486+
return self
487+
488+
if self.anatom_site_general.value not in valid_combinations[self.anatom_site_special.value]:
489+
raise error_incompatible_fields(
490+
"anatom_site_special",
491+
"anatom_site_general",
492+
self.anatom_site_special.value,
493+
self.anatom_site_general.value,
494+
)
495+
496+
return self
497+
445498
@model_validator(mode="after")
446499
def validate_tbp_tile_fields(self) -> MetadataRow:
447500
if not self.tbp_tile_type:

tests/test_fields.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import pytest
88

99
from isic_metadata.diagnosis_hierarchical import DiagnosisEnum
10+
from isic_metadata.fields import AnatomSiteGeneralEnum
1011
from isic_metadata.metadata import MetadataRow, convert_errors
1112

1213

@@ -124,3 +125,45 @@ def test_clin_size_long_diam_mm_invalid():
124125
MetadataRow.model_validate({"clin_size_long_diam_mm": "foo"})
125126
assert len(excinfo.value.errors()) == 1
126127
assert "Unable to parse value as a number" in convert_errors(excinfo.value)[0]["msg"]
128+
129+
130+
@pytest.mark.parametrize(
131+
("anatom_site_special", "anatom_site_general_values"),
132+
[
133+
("acral NOS", ["upper extremity", "lower extremity", "palms/soles"]),
134+
("nail NOS", ["upper extremity", "lower extremity", "palms/soles"]),
135+
("fingernail", ["upper extremity", "palms/soles"]),
136+
("toenail", ["lower extremity", "palms/soles"]),
137+
("acral palms or soles", ["upper extremity", "lower extremity", "palms/soles"]),
138+
("oral or genital", ["head/neck", "oral/genital"]),
139+
],
140+
)
141+
def test_anatom_site_special(anatom_site_special: str, anatom_site_general_values: list[str]):
142+
for anatom_site_general_value in anatom_site_general_values:
143+
metadata = MetadataRow.model_validate(
144+
{
145+
"anatom_site_special": anatom_site_special,
146+
"anatom_site_general": anatom_site_general_value,
147+
}
148+
)
149+
assert metadata.anatom_site_special == anatom_site_special
150+
assert metadata.anatom_site_general == anatom_site_general_value
151+
152+
for invalid_anatom_site_general in AnatomSiteGeneralEnum:
153+
if invalid_anatom_site_general.value not in anatom_site_general_values:
154+
with pytest.raises(ValidationError) as excinfo:
155+
MetadataRow.model_validate(
156+
{
157+
"anatom_site_special": anatom_site_special,
158+
"anatom_site_general": invalid_anatom_site_general.value,
159+
}
160+
)
161+
assert len(excinfo.value.errors()) == 1
162+
assert "is incompatible with anatom_site_general" in excinfo.value.errors()[0]["msg"]
163+
164+
165+
def test_anatom_site_special_requires_anatom_site_general():
166+
with pytest.raises(ValidationError) as excinfo:
167+
MetadataRow.model_validate({"anatom_site_special": "acral NOS"})
168+
assert len(excinfo.value.errors()) == 1
169+
assert "requires setting anatom_site_general" in excinfo.value.errors()[0]["msg"]

0 commit comments

Comments
 (0)