diff --git a/fairscape_models/__init__.py b/fairscape_models/__init__.py index fa64eb0..dac6ce6 100644 --- a/fairscape_models/__init__.py +++ b/fairscape_models/__init__.py @@ -1,8 +1,11 @@ +from fairscape_models.activity import Activity +from fairscape_models.digital_object import DigitalObject from fairscape_models.annotation import Annotation from fairscape_models.biochem_entity import BioChemEntity from fairscape_models.computation import Computation from fairscape_models.dataset import Dataset from fairscape_models.software import Software +from fairscape_models.mlmodel import MLModel from fairscape_models.fairscape_base import IdentifierValue, IdentifierPropertyValue from fairscape_models.medical_condition import MedicalCondition from fairscape_models.schema import Schema diff --git a/fairscape_models/activity.py b/fairscape_models/activity.py new file mode 100644 index 0000000..0a3e402 --- /dev/null +++ b/fairscape_models/activity.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional, List + +from fairscape_models.fairscape_base import IdentifierValue + +class Activity(BaseModel): + """Base class for Activity types (Computation, Annotation, Experiment)""" + guid: str = Field(alias="@id") + name: str + metadataType: Optional[str] = Field(default=None, alias="@type") + description: str = Field(min_length=10) + associatedPublication: Optional[str] = Field(default=None) + generated: Optional[List[IdentifierValue]] = Field(default=[]) + isPartOf: Optional[List[IdentifierValue]] = Field(default=[]) + + model_config = ConfigDict(extra="allow") diff --git a/fairscape_models/annotation.py b/fairscape_models/annotation.py index eab9155..773bf37 100644 --- a/fairscape_models/annotation.py +++ b/fairscape_models/annotation.py @@ -1,19 +1,12 @@ -from pydantic import BaseModel, Field, ConfigDict +from pydantic import Field, ConfigDict from typing import Optional, List from fairscape_models.fairscape_base import IdentifierValue, ANNOTATION_TYPE +from fairscape_models.activity import Activity -class Annotation(BaseModel): - guid: str = Field(alias="@id") - name: str +class Annotation(Activity): metadataType: Optional[str] = Field(default="https://w3id.org/EVI#Annotation", alias="@type") additionalType: Optional[str] = Field(default=ANNOTATION_TYPE) createdBy: str - description: str = Field(min_length=10) dateCreated: str - associatedPublication: Optional[str] = Field(default=None) usedDataset: Optional[List[IdentifierValue]] = Field(default=[]) - generated: Optional[List[IdentifierValue]] = Field(default=[]) - isPartOf: Optional[List[IdentifierValue]] = Field(default=[]) - - model_config = ConfigDict(extra="allow") diff --git a/fairscape_models/computation.py b/fairscape_models/computation.py index 852b845..50332ad 100644 --- a/fairscape_models/computation.py +++ b/fairscape_models/computation.py @@ -1,23 +1,16 @@ -from pydantic import BaseModel, Field, ConfigDict +from pydantic import Field, ConfigDict from typing import Optional, List, Union from fairscape_models.fairscape_base import IdentifierValue, COMPUTATION_TYPE +from fairscape_models.activity import Activity -class Computation(BaseModel): - guid: str = Field(alias="@id") - name: str - metadataType: Optional[str] = Field(default="https://w3id.org/EVI#Computation",alias="@type") +class Computation(Activity): + metadataType: Optional[str] = Field(default="https://w3id.org/EVI#Computation", alias="@type") additionalType: Optional[str] = Field(default=COMPUTATION_TYPE) runBy: str - description: str = Field(min_length=10) dateCreated: str - associatedPublication: Optional[str] = Field(default=None) additionalDocumentation: Optional[str] = Field(default=None) command: Optional[Union[List[str], str]] = Field(default=None) usedSoftware: Optional[List[IdentifierValue]] = Field(default=[]) usedMLModel: Optional[List[IdentifierValue]] = Field(default=[]) usedDataset: Optional[List[IdentifierValue]] = Field(default=[]) - generated: Optional[List[IdentifierValue]] = Field(default=[]) - isPartOf: Optional[List[IdentifierValue]] = Field(default=[]) - - model_config = ConfigDict(extra="allow") diff --git a/fairscape_models/dataset.py b/fairscape_models/dataset.py index 30188e8..69c2d3f 100644 --- a/fairscape_models/dataset.py +++ b/fairscape_models/dataset.py @@ -1,30 +1,19 @@ -from pydantic import BaseModel, Field, ConfigDict, AliasChoices, field_validator +from pydantic import Field, ConfigDict, AliasChoices from typing import Optional, List, Union from fairscape_models.fairscape_base import IdentifierValue, DATASET_TYPE +from fairscape_models.digital_object import DigitalObject -class Dataset(BaseModel): - guid: str = Field(alias="@id") - name: str - metadataType: Optional[str] = Field(default="https://w3id.org/EVI#Dataset",alias="@type") +class Dataset(DigitalObject): + metadataType: Optional[str] = Field(default="https://w3id.org/EVI#Dataset", alias="@type") additionalType: Optional[str] = Field(default=DATASET_TYPE) - author: Union[str, List[str]] datePublished: str = Field(...) - version: str = Field(default="0.1.0") - description: str = Field(min_length=10) keywords: List[str] = Field(...) - associatedPublication: Optional[Union[str,List[str]]] = Field(default=None) - additionalDocumentation: Optional[str] = Field(default=None) fileFormat: str = Field(alias="format") dataSchema: Optional[IdentifierValue] = Field( - validation_alias=AliasChoices('evi:Schema', 'EVI:Schema', 'schema','evi:schema'), + validation_alias=AliasChoices('evi:Schema', 'EVI:Schema', 'schema', 'evi:schema'), serialization_alias='evi:Schema', default=None ) generatedBy: Optional[Union[IdentifierValue, List[IdentifierValue]]] = Field(default=[]) - derivedFrom: Optional[List[IdentifierValue]] = Field(default=[]) - usedByComputation: Optional[List[IdentifierValue]] = Field(default=[]) - contentUrl: Optional[Union[str, List[str]]] = Field(default=None) - isPartOf: Optional[List[IdentifierValue]] = Field(default=[]) - - model_config = ConfigDict(extra="allow") \ No newline at end of file + derivedFrom: Optional[List[IdentifierValue]] = Field(default=[]) \ No newline at end of file diff --git a/fairscape_models/digital_object.py b/fairscape_models/digital_object.py new file mode 100644 index 0000000..755c0fd --- /dev/null +++ b/fairscape_models/digital_object.py @@ -0,0 +1,20 @@ +from pydantic import BaseModel, Field, ConfigDict +from typing import Optional, List, Union + +from fairscape_models.fairscape_base import IdentifierValue + +class DigitalObject(BaseModel): + """Base class for DigitalObject types (Dataset, Software, MLModel)""" + guid: str = Field(alias="@id") + name: str + metadataType: Optional[str] = Field(default=None, alias="@type") + author: Union[str, List[str]] + description: str = Field(min_length=10) + version: str = Field(default="0.1.0") + associatedPublication: Optional[Union[str, List[str]]] = Field(default=None) + additionalDocumentation: Optional[str] = Field(default=None) + contentUrl: Optional[Union[str, List[str]]] = Field(default=None) + isPartOf: Optional[List[IdentifierValue]] = Field(default=[]) + usedByComputation: Optional[List[IdentifierValue]] = Field(default=[]) + + model_config = ConfigDict(extra="allow") diff --git a/fairscape_models/experiment.py b/fairscape_models/experiment.py index a7a6db9..e5e4046 100644 --- a/fairscape_models/experiment.py +++ b/fairscape_models/experiment.py @@ -1,21 +1,15 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional, List, Union +from pydantic import Field, ConfigDict +from typing import Optional, List from fairscape_models.fairscape_base import IdentifierValue +from fairscape_models.activity import Activity -class Experiment(BaseModel): - guid: str = Field(alias="@id") - name: str +class Experiment(Activity): metadataType: Optional[str] = Field(default="https://w3id.org/EVI#Experiment", alias="@type") - experimentType: str + experimentType: str runBy: str - description: str = Field(min_length=10) - datePerformed: str - associatedPublication: Optional[str] = Field(default=None) - protocol: Optional[str] = Field(default=None) - usedInstrument: Optional[List[IdentifierValue]] = Field(default=[]) - usedSample: Optional[List[IdentifierValue]] = Field(default=[]) + datePerformed: str + protocol: Optional[str] = Field(default=None) + usedInstrument: Optional[List[IdentifierValue]] = Field(default=[]) + usedSample: Optional[List[IdentifierValue]] = Field(default=[]) usedTreatment: Optional[List[IdentifierValue]] = Field(default=[]) - usedStain: Optional[List[IdentifierValue]] = Field(default=[]) - generated: Optional[List[IdentifierValue]] = Field(default=[]) - isPartOf: Optional[List[IdentifierValue]] = Field(default=[]) - model_config = ConfigDict(extra="allow") \ No newline at end of file + usedStain: Optional[List[IdentifierValue]] = Field(default=[]) \ No newline at end of file diff --git a/fairscape_models/fairscape_base.py b/fairscape_models/fairscape_base.py index 91e21e5..132bdc2 100644 --- a/fairscape_models/fairscape_base.py +++ b/fairscape_models/fairscape_base.py @@ -20,6 +20,7 @@ DATASET_TYPE = "Dataset" DATASET_CONTAINER_TYPE = "DatasetContainer" SOFTWARE_TYPE = "Software" +MLMODEL_TYPE = "MLModel" COMPUTATION_TYPE = "Computation" ANNOTATION_TYPE = "Annotation" ROCRATE_TYPE = "ROCrate" @@ -58,6 +59,7 @@ class ClassType(str, Enum): DATASET = 'Dataset' SOFTWARE = 'Software' + MLMODEL = 'MLModel' COMPUTATION = 'Computation' ANNOTATION = 'Annotation' SCHEMA = 'Schema' diff --git a/fairscape_models/mlmodel.py b/fairscape_models/mlmodel.py new file mode 100644 index 0000000..7ff56bd --- /dev/null +++ b/fairscape_models/mlmodel.py @@ -0,0 +1,14 @@ +from pydantic import Field, ConfigDict +from typing import Optional, List, Union + +from fairscape_models.fairscape_base import IdentifierValue, MLMODEL_TYPE +from fairscape_models.digital_object import DigitalObject + +class MLModel(DigitalObject): + metadataType: Optional[str] = Field(default="https://w3id.org/EVI#MLModel", alias="@type") + additionalType: Optional[str] = Field(default=MLMODEL_TYPE) + dateModified: Optional[str] = Field(default=None) + fileFormat: str = Field(alias="format") + modelTask: Optional[str] = Field(default=None) + modelArchitecture: Optional[str] = Field(default=None) + trainedOn: Optional[List[IdentifierValue]] = Field(default=[]) diff --git a/fairscape_models/rocrate.py b/fairscape_models/rocrate.py index e6e7763..1ce1010 100644 --- a/fairscape_models/rocrate.py +++ b/fairscape_models/rocrate.py @@ -8,8 +8,11 @@ from fairscape_models.biochem_entity import BioChemEntity from fairscape_models.medical_condition import MedicalCondition from fairscape_models.computation import Computation +from fairscape_models.annotation import Annotation +from fairscape_models.experiment import Experiment from fairscape_models.dataset import Dataset from fairscape_models.software import Software +from fairscape_models.mlmodel import MLModel from fairscape_models.patient import Patient class GenericMetadataElem(BaseModel): @@ -127,7 +130,10 @@ class ROCrateV1_2(BaseModel): metadataGraph: List[Union[ Dataset, Software, + MLModel, Computation, + Annotation, + Experiment, ROCrateMetadataElem, ROCrateMetadataFileElem, Schema, @@ -146,7 +152,10 @@ def validate_metadata_graph(cls, values: Dict[str, Any]) -> Dict[str, Any]: type_map = { "Dataset": Dataset, "Software": Software, + "MLModel": MLModel, "Computation": Computation, + "Annotation": Annotation, + "Experiment": Experiment, "CreativeWork": ROCrateMetadataFileElem, "Schema": Schema, "BioChemEntity": BioChemEntity, @@ -206,8 +215,6 @@ def cleanIdentifierList(identifier_list): """Helper to clean a list of identifiers""" if identifier_list is None: return - if not isinstance(identifier_list, list): - return for item in identifier_list: if hasattr(item, 'guid') and isinstance(item.guid, str) and "ark:" in item.guid: cleanGUID(item) @@ -241,6 +248,11 @@ def cleanIdentifierUnion(identifier_union): if isinstance(elem, Software): cleanIdentifierList(elem.usedByComputation) + if isinstance(elem, MLModel): + cleanIdentifierList(elem.usedByComputation) + + cleanIdentifierList(elem.trainedOn) + if isinstance(elem, Computation): cleanIdentifierList(elem.usedDataset) @@ -249,6 +261,26 @@ def cleanIdentifierUnion(identifier_union): cleanIdentifierList(elem.usedSoftware) + cleanIdentifierList(elem.usedMLModel) + + if isinstance(elem, Annotation): + + cleanIdentifierList(elem.usedDataset) + + cleanIdentifierList(elem.generated) + + if isinstance(elem, Experiment): + + cleanIdentifierList(elem.usedInstrument) + + cleanIdentifierList(elem.usedSample) + + cleanIdentifierList(elem.usedTreatment) + + cleanIdentifierList(elem.usedStain) + + cleanIdentifierList(elem.generated) + def getCrateMetadata(self)-> ROCrateMetadataElem: """ Filter the Metadata Graph for the Metadata Element Describing the Toplevel ROCrate @@ -316,7 +348,49 @@ def getComputations(self) -> List[Computation]: :rtype List[fairscape_mds.models.rocrate.Computation] """ filterResults = list(filter( - lambda x: isinstance(x, Computation), + lambda x: isinstance(x, Computation), + self.metadataGraph + )) + + return filterResults + + def getAnnotations(self) -> List[Annotation]: + """ Filter the Metadata Graph for Annotation Elements + + :param self + :return: All Annotation metadata records within the ROCrate + :rtype List[fairscape_mds.models.rocrate.Annotation] + """ + filterResults = list(filter( + lambda x: isinstance(x, Annotation), + self.metadataGraph + )) + + return filterResults + + def getExperiments(self) -> List[Experiment]: + """ Filter the Metadata Graph for Experiment Elements + + :param self + :return: All Experiment metadata records within the ROCrate + :rtype List[fairscape_mds.models.rocrate.Experiment] + """ + filterResults = list(filter( + lambda x: isinstance(x, Experiment), + self.metadataGraph + )) + + return filterResults + + def getMLModels(self) -> List[MLModel]: + """ Filter the Metadata Graph for MLModel Elements + + :param self + :return: All MLModel metadata records within the ROCrate + :rtype List[fairscape_mds.models.rocrate.MLModel] + """ + filterResults = list(filter( + lambda x: isinstance(x, MLModel), self.metadataGraph )) @@ -354,13 +428,18 @@ def getMedicalConditions(self) -> List[MedicalCondition]: def getEVIElements(self) -> List[Union[ - Computation, - Dataset, - Software, + Computation, + Annotation, + Experiment, + Dataset, + Software, + MLModel, Schema, BioChemEntity, MedicalCondition ]]: """ Query the metadata graph for elements which require minting identifiers """ - return self.getDatasets() + self.getSoftware() + self.getComputations() + self.getSchemas() + return (self.getDatasets() + self.getSoftware() + self.getMLModels() + + self.getComputations() + self.getAnnotations() + self.getExperiments() + + self.getSchemas()) diff --git a/fairscape_models/software.py b/fairscape_models/software.py index 10c4b5a..2622d03 100644 --- a/fairscape_models/software.py +++ b/fairscape_models/software.py @@ -1,22 +1,11 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import Optional, List, Union +from pydantic import Field, ConfigDict +from typing import Optional, List from fairscape_models.fairscape_base import IdentifierValue, SOFTWARE_TYPE +from fairscape_models.digital_object import DigitalObject -class Software(BaseModel): - guid: str = Field(alias="@id") - name: str +class Software(DigitalObject): metadataType: Optional[str] = Field(default="https://w3id.org/EVI#Software", alias="@type") additionalType: Optional[str] = Field(default=SOFTWARE_TYPE) - author: str = Field(min_length=4) dateModified: Optional[str] - version: str = Field(default="0.1.0") - description: str = Field(min_length=10) - associatedPublication: Optional[str] = Field(default=None) - additionalDocumentation: Optional[str] = Field(default=None) fileFormat: str = Field(title="fileFormat", alias="format") - usedByComputation: Optional[List[IdentifierValue]] = Field(default=[]) - contentUrl: Optional[str] = Field(default=None) - isPartOf: Optional[List[IdentifierValue]] = Field(default=[]) - - model_config = ConfigDict(extra="allow") diff --git a/pyproject.toml b/pyproject.toml index dd56db8..554db7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "fairscape-models" -version = "1.0.15" +version = "1.0.19" description = "Fairscape pydantic models" readme = "README.md" authors = [ diff --git a/tests/test_rocrate_validation.py b/tests/test_rocrate_validation.py index 7ecb6be..7be8079 100644 --- a/tests/test_rocrate_validation.py +++ b/tests/test_rocrate_validation.py @@ -13,6 +13,9 @@ from fairscape_models.dataset import Dataset from fairscape_models.software import Software from fairscape_models.computation import Computation +from fairscape_models.mlmodel import MLModel +from fairscape_models.annotation import Annotation +from fairscape_models.experiment import Experiment # Define the path to the Test-ROcrates directory TEST_ROCRATES_PATH = pathlib.Path(__file__).parent / "test_rocrates" @@ -190,4 +193,189 @@ def test_get_medical_conditions(comprehensive_rocrate_data): conditions = rocrate.getMedicalConditions() assert len(conditions) == 1 assert isinstance(conditions[0], MedicalCondition) - assert conditions[0].guid == "ark:59852/test-condition" \ No newline at end of file + assert conditions[0].guid == "ark:59852/test-condition" + + +def test_clean_identifiers_with_none_fields(): + """Test cleanIdentifiers with None fields to ensure it doesn't crash.""" + data = { + "@context": {}, + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + "about": {"@id": "ark:59852/test-crate"} + }, + { + "@id": "ark:59852/test-crate", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "name": "Test Crate", "description": "A test crate for validation", "keywords": [], + "version": "1.0", "author": "tester", "license": "MIT", + "hasPart": [{"@id": "ark:59852/test-dataset"}] + }, + { + "@id": "ark:59852/test-dataset", + "@type": "https://w3id.org/EVI#Dataset", + "name": "Test Dataset", "author": "tester", "datePublished": "2024-01-01", + "description": "A test dataset", "keywords": [], "format": "text/plain", + "usedByComputation": None, # None field + "generatedBy": None # None field + } + ] + } + rocrate = ROCrateV1_2.model_validate(data) + # Should not crash + rocrate.cleanIdentifiers() + assert True + + +def test_clean_identifiers_with_single_identifier(): + """Test cleanIdentifiers with single IdentifierValue (not a list) in generatedBy.""" + data = { + "@context": {}, + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + "about": {"@id": "ark:59852/test-crate"} + }, + { + "@id": "ark:59852/test-crate", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "name": "Test Crate", "description": "A test crate for validation", "keywords": [], + "version": "1.0", "author": "tester", "license": "MIT", + "hasPart": [{"@id": "ark:59852/test-dataset"}] + }, + { + "@id": "https://fairscape.net/ark:59852/test-dataset", + "@type": "https://w3id.org/EVI#Dataset", + "name": "Test Dataset", "author": "tester", "datePublished": "2024-01-01", + "description": "A test dataset", "keywords": [], "format": "text/plain", + "generatedBy": {"@id": "https://fairscape.net/ark:59852/test-computation"} # Single identifier, not list + } + ] + } + rocrate = ROCrateV1_2.model_validate(data) + rocrate.cleanIdentifiers() + dataset = rocrate.getDatasets()[0] + assert dataset.generatedBy.guid == "ark:59852/test-computation" + + +def test_clean_identifiers_with_mlmodel(): + """Test cleanIdentifiers with MLModel elements.""" + data = { + "@context": {}, + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + "about": {"@id": "ark:59852/test-crate"} + }, + { + "@id": "ark:59852/test-crate", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "name": "Test Crate", "description": "A test crate for validation", "keywords": [], + "version": "1.0", "author": "tester", "license": "MIT", + "hasPart": [{"@id": "ark:59852/test-mlmodel"}] + }, + { + "@id": "https://fairscape.net/ark:59852/test-mlmodel", + "@type": "https://w3id.org/EVI#MLModel", + "name": "Test ML Model", "author": "tester", "datePublished": "2024-01-01", + "description": "A test ML model", "format": "application/x-hdf5", + "usedByComputation": [{"@id": "https://fairscape.net/ark:59852/test-computation"}], + "trainedOn": [{"@id": "https://fairscape.net/ark:59852/test-dataset"}] + } + ] + } + rocrate = ROCrateV1_2.model_validate(data) + rocrate.cleanIdentifiers() + mlmodel = rocrate.getMLModels()[0] + assert mlmodel.guid == "ark:59852/test-mlmodel" + assert mlmodel.usedByComputation[0].guid == "ark:59852/test-computation" + assert mlmodel.trainedOn[0].guid == "ark:59852/test-dataset" + + +def test_clean_identifiers_with_annotation(): + """Test cleanIdentifiers with Annotation elements.""" + data = { + "@context": {}, + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + "about": {"@id": "ark:59852/test-crate"} + }, + { + "@id": "ark:59852/test-crate", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "name": "Test Crate", "description": "A test crate for validation", "keywords": [], + "version": "1.0", "author": "tester", "license": "MIT", + "hasPart": [{"@id": "ark:59852/test-annotation"}] + }, + { + "@id": "https://fairscape.net/ark:59852/test-annotation", + "@type": "https://w3id.org/EVI#Annotation", + "name": "Test Annotation", "author": "tester", "dateCreated": "2024-01-01", + "description": "A test annotation", + "createdBy": "tester", + "usedDataset": [{"@id": "https://fairscape.net/ark:59852/test-dataset"}], + "generated": [{"@id": "https://fairscape.net/ark:59852/test-output"}] + } + ] + } + rocrate = ROCrateV1_2.model_validate(data) + rocrate.cleanIdentifiers() + annotation = rocrate.getAnnotations()[0] + assert annotation.guid == "ark:59852/test-annotation" + assert annotation.usedDataset[0].guid == "ark:59852/test-dataset" + assert annotation.generated[0].guid == "ark:59852/test-output" + + +def test_clean_identifiers_with_experiment(): + """Test cleanIdentifiers with Experiment elements.""" + data = { + "@context": {}, + "@graph": [ + { + "@id": "ro-crate-metadata.json", + "@type": "CreativeWork", + "conformsTo": {"@id": "https://w3id.org/ro/crate/1.1"}, + "about": {"@id": "ark:59852/test-crate"} + }, + { + "@id": "ark:59852/test-crate", + "@type": ["Dataset", "https://w3id.org/EVI#ROCrate"], + "name": "Test Crate", "description": "A test crate for validation", "keywords": [], + "version": "1.0", "author": "tester", "license": "MIT", + "hasPart": [{"@id": "ark:59852/test-experiment"}] + }, + { + "@id": "https://fairscape.net/ark:59852/test-experiment", + "@type": "https://w3id.org/EVI#Experiment", + "name": "Test Experiment", "author": "tester", "dateCreated": "2024-01-01", + "description": "A test experiment", + "experimentType": "microscopy", + "runBy": "tester", + "datePerformed": "2024-01-01", + "usedInstrument": [{"@id": "https://fairscape.net/ark:59852/test-instrument"}], + "usedSample": [{"@id": "https://fairscape.net/ark:59852/test-sample"}], + "usedTreatment": [{"@id": "https://fairscape.net/ark:59852/test-treatment"}], + "usedStain": [{"@id": "https://fairscape.net/ark:59852/test-stain"}], + "generated": [{"@id": "https://fairscape.net/ark:59852/test-result"}] + } + ] + } + rocrate = ROCrateV1_2.model_validate(data) + rocrate.cleanIdentifiers() + experiment = rocrate.getExperiments()[0] + assert experiment.guid == "ark:59852/test-experiment" + assert experiment.usedInstrument[0].guid == "ark:59852/test-instrument" + assert experiment.usedSample[0].guid == "ark:59852/test-sample" + assert experiment.usedTreatment[0].guid == "ark:59852/test-treatment" + assert experiment.usedStain[0].guid == "ark:59852/test-stain" + assert experiment.generated[0].guid == "ark:59852/test-result" \ No newline at end of file