diff --git a/dandi/metadata/util.py b/dandi/metadata/util.py index ab294a572..21833d859 100644 --- a/dandi/metadata/util.py +++ b/dandi/metadata/util.py @@ -288,10 +288,18 @@ def timedelta2duration(delta: timedelta) -> str: return s +# Names for the sex IRIs which ``extract_sex`` produces itself +SEX_NAMES_BY_IRI = { + "http://purl.obolibrary.org/obo/PATO_0000384": "Male", + "http://purl.obolibrary.org/obo/PATO_0000383": "Female", +} + + def extract_sex(metadata: dict) -> models.SexType | None: value = metadata.get("sex", None) if value is not None and value != "": - value = value.lower() + value_orig = value.strip() + value = value_orig.lower() if value in ["m", "male"]: value_id = "http://purl.obolibrary.org/obo/PATO_0000384" value = "Male" @@ -305,8 +313,11 @@ def extract_sex(metadata: dict) -> models.SexType | None: value_id = None value = "Other" elif value.startswith("http"): - value_id = value - value = None + value_id = value_orig + # dandischema requires a name; fall back to the IRI itself + value = {k.lower(): v for k, v in SEX_NAMES_BY_IRI.items()}.get( + value, value_id + ) else: raise ValueError(f"Cannot interpret sex field: {value}") return models.SexType(identifier=value_id, name=value) @@ -597,7 +608,8 @@ def extract_species(metadata: dict) -> models.SpeciesType | None: value = " - ".join( [result[key] for key in lookup if key in result] ) - value_matches.append((value_id, value)) + # dandischema requires a name; fall back to the IRI itself + value_matches.append((value_id, value or value_id)) else: lower_value = value_orig.lower().strip() for record in species_map: diff --git a/dandi/tests/test_metadata.py b/dandi/tests/test_metadata.py index f6b40ddaa..9e1a716d6 100644 --- a/dandi/tests/test_metadata.py +++ b/dandi/tests/test_metadata.py @@ -51,6 +51,7 @@ SpeciesRecord, extract_age, extract_cellLine, + extract_sex, extract_species, parse_age, parse_purlobourl, @@ -887,6 +888,54 @@ def test_species_rat(species: str) -> None: } +@pytest.mark.ai_generated +def test_species_unknown_iri_lookup_fails(monkeypatch: pytest.MonkeyPatch) -> None: + # An NCBITaxon IRI not in species_map whose label cannot be looked up + # still gets a name, as dandischema requires one + iri = "http://purl.obolibrary.org/obo/NCBITaxon_999999999" + + def fail(*_args: Any, **_kwargs: Any) -> None: + raise ConnectionError("no network") + + monkeypatch.setattr("dandi.metadata.util.parse_purlobourl", fail) + species_rec = extract_species({"species": iri}) + assert species_rec is not None + assert str(species_rec.identifier) == iri + assert species_rec.name == iri + + +@pytest.mark.ai_generated +@pytest.mark.parametrize( + "sex,identifier,name", + [ + ("M", "http://purl.obolibrary.org/obo/PATO_0000384", "Male"), + ("female", "http://purl.obolibrary.org/obo/PATO_0000383", "Female"), + ("U", None, "Unknown"), + ( + "http://purl.obolibrary.org/obo/PATO_0000384", + "http://purl.obolibrary.org/obo/PATO_0000384", + "Male", + ), + ( + "http://purl.obolibrary.org/obo/PATO_0000383", + "http://purl.obolibrary.org/obo/PATO_0000383", + "Female", + ), + # unknown IRI: kept as is (not lowercased) and used as the name + ( + "http://purl.obolibrary.org/obo/PATO_0001340", + "http://purl.obolibrary.org/obo/PATO_0001340", + "http://purl.obolibrary.org/obo/PATO_0001340", + ), + ], +) +def test_extract_sex(sex: str, identifier: str | None, name: str) -> None: + rec = extract_sex({"sex": sex}) + assert rec is not None + assert (None if rec.identifier is None else str(rec.identifier)) == identifier + assert rec.name == name + + @pytest.mark.parametrize( "species", [ diff --git a/pyproject.toml b/pyproject.toml index 7a6ba7b9b..2b580e080 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -50,7 +50,7 @@ dependencies = [ # Exclude 0.13.0: it lacks the downgrade migrations (SIMPLE_DOWNGRADES # entries for releaseNotes / sameAs) that the client relies on to talk # to older Archive servers. 0.14.0 restored them. - "dandischema >= 0.12.0, != 0.13.0, < 0.15.0", + "dandischema >= 0.12.0, != 0.13.0, < 0.16.0", "etelemetry >= 0.2.2", "fasteners >= 0.19", "fscacher >= 0.3.0",