-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
created attribute subclasses for used across BIA packages (#294)
* created attribute subclasses for used across BIA packages * removed to_pascal
- Loading branch information
Showing
7 changed files
with
246 additions
and
22 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
109 changes: 109 additions & 0 deletions
109
bia-shared-datamodels/src/bia_shared_datamodels/attribute_models.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,109 @@ | ||
from __future__ import annotations | ||
|
||
from typing import List, Optional, Any | ||
from typing_extensions import Self | ||
|
||
from pydantic import BaseModel, Field, model_validator, field_validator, ValidationError | ||
from .semantic_models import Attribute, AttributeProvenance | ||
from uuid import UUID | ||
|
||
# For shared models that are placed inside Attribute objects | ||
# The API does not enforce what is created inside an Attribute object's value (beyond it being a dicitionary). | ||
# This allows us to have additional freeform information from submitters. | ||
# Model here are for use by BIA internal code packages that require a shared source of truth. | ||
|
||
|
||
class SubAttributeMixin(BaseModel): | ||
def __eq__(self, other): | ||
if other.__class__ == Attribute: | ||
attribute_self = Attribute.model_validate(self.model_dump()) | ||
return attribute_self.__eq__(other) | ||
else: | ||
return BaseModel.__eq__(self, other) | ||
|
||
|
||
class DatasetAssociationValue(BaseModel): | ||
# Allows None, but requires fields to be present | ||
image_analysis: Optional[str] = Field() | ||
image_correlation: Optional[str] = Field() | ||
biosample: Optional[str] = Field() | ||
image_acquisition: Optional[str] = Field() | ||
specimen: Optional[str] = Field() | ||
|
||
|
||
class DatasetAssociationAttribute(Attribute, SubAttributeMixin): | ||
""" | ||
Model for storing user provided Associations from biostudies in an Attribute on a dataset. | ||
""" | ||
|
||
@field_validator("provenance", mode="after") | ||
@classmethod | ||
def attribute_provenance(cls, value: AttributeProvenance) -> AttributeProvenance: | ||
if value != AttributeProvenance.bia_ingest: | ||
raise ValueError( | ||
f"Provenance for this type of attribute must be {AttributeProvenance.bia_ingest}" | ||
) | ||
|
||
return value | ||
|
||
@field_validator("name", mode="after") | ||
@classmethod | ||
def attribute_name(cls, value: str) -> str: | ||
if value != "associations": | ||
raise ValueError( | ||
f"name field for this type of attribute must be 'associations'" | ||
) | ||
return value | ||
|
||
@field_validator("value", mode="after") | ||
@classmethod | ||
def attribute_value(cls, value: dict) -> dict: | ||
if len(value.keys()) != 1: | ||
raise ValueError("Value dictionary should have exactly one 1 key") | ||
elif "associations" not in value.keys(): | ||
raise ValueError(f'The value dictionary key must be "associations"') | ||
return value | ||
|
||
value: dict[str, list[DatasetAssociationValue]] = Field() | ||
|
||
|
||
class DatasetAssociatedUUIDAttribute(Attribute, SubAttributeMixin): | ||
""" | ||
Model for storing uuid of objects linked to a dataset, for use in code downstream of ingest. | ||
""" | ||
|
||
@field_validator("provenance", mode="after") | ||
@classmethod | ||
def attribute_provenance(cls, value: AttributeProvenance) -> AttributeProvenance: | ||
if value != AttributeProvenance.bia_ingest: | ||
raise ValueError( | ||
f"Provenance for this type of attribute must be {AttributeProvenance.bia_ingest}" | ||
) | ||
return value | ||
|
||
@field_validator("name", mode="after") | ||
@classmethod | ||
def validate_attribute_name(cls, value) -> Self: | ||
valid_associations = [ | ||
"image_acquisition_protocol_uuid", | ||
"specimen_imaging_preparation_protocol_uuid", | ||
"bio_sample_uuid", | ||
"annotation_method_uuid", | ||
"protocol_uuid", | ||
] | ||
if value not in valid_associations: | ||
raise ValueError( | ||
f"Name for this type of attribute must be one of: {valid_associations}" | ||
) | ||
|
||
return value | ||
|
||
@model_validator(mode="after") | ||
def validate_attribute_value_key(self) -> Self: | ||
if len(self.value.keys()) != 1: | ||
raise ValueError("Value dictionary should have exactly one 1 key") | ||
elif self.name not in self.value.keys(): | ||
raise ValueError(f"The key for this type of attribute must be {self.name}") | ||
return self | ||
|
||
value: dict[str, list[str]] = Field() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import pytest | ||
from pydantic import ValidationError | ||
from bia_shared_datamodels import ( | ||
semantic_models, | ||
mock_objects, | ||
attribute_models, | ||
) | ||
from typing import Callable | ||
|
||
|
||
@pytest.mark.parametrize( | ||
("expected_model_type", "dict_creation_func"), | ||
( | ||
( | ||
attribute_models.DatasetAssociatedUUIDAttribute, | ||
mock_objects.get_dataset_associated_uuid_attribute, | ||
), | ||
( | ||
attribute_models.DatasetAssociationAttribute, | ||
mock_objects.get_dataset_associatation_attribute, | ||
), | ||
), | ||
) | ||
def test_sub_attribute_models( | ||
expected_model_type: semantic_models.Attribute, | ||
dict_creation_func: Callable[[mock_objects.Completeness], dict], | ||
): | ||
|
||
model_completeness_list = [ | ||
mock_objects.Completeness.COMPLETE, | ||
mock_objects.Completeness.MINIMAL, | ||
] | ||
|
||
for model_completion in model_completeness_list: | ||
|
||
model_dict = dict_creation_func(model_completion) | ||
|
||
attribute_model = semantic_models.Attribute.model_validate(model_dict) | ||
sub_attribute_model = expected_model_type.model_validate(model_dict) | ||
|
||
# We have modified the __eq__ function, so it is good to check: | ||
assert sub_attribute_model == sub_attribute_model | ||
|
||
assert attribute_model.model_dump() == sub_attribute_model.model_dump() | ||
|
||
assert attribute_model == sub_attribute_model | ||
|
||
assert attribute_model == semantic_models.Attribute.model_validate( | ||
sub_attribute_model | ||
) | ||
|
||
# Check basic attribute doesn't pass validation | ||
try: | ||
expected_model_type.model_validate( | ||
mock_objects.get_attribute_dict(model_completion) | ||
) | ||
assert False | ||
except ValidationError: | ||
assert True |