From b08e0bb369b2450afe3aa9e0492a2bacff947b13 Mon Sep 17 00:00:00 2001 From: Liviu Anita Date: Thu, 25 Jul 2024 15:05:37 +0100 Subject: [PATCH] Add model metadata to objects, object version, shared exceptions --- .../bia_shared_datamodels/bia_data_model.py | 110 ++++++++++++------ .../src/bia_shared_datamodels/exceptions.py | 5 + 2 files changed, 80 insertions(+), 35 deletions(-) create mode 100644 bia-shared-datamodels/src/bia_shared_datamodels/exceptions.py diff --git a/bia-shared-datamodels/src/bia_shared_datamodels/bia_data_model.py b/bia-shared-datamodels/src/bia_shared_datamodels/bia_data_model.py index 730d7374..4e5074e2 100644 --- a/bia-shared-datamodels/src/bia_shared_datamodels/bia_data_model.py +++ b/bia-shared-datamodels/src/bia_shared_datamodels/bia_data_model.py @@ -1,17 +1,52 @@ from __future__ import annotations from . import semantic_models -from pydantic import BaseModel, Field -from typing import List, Optional +from . import exceptions +from pydantic import BaseModel, ConfigDict, Field, AnyUrl +from typing import List, Optional, Union from uuid import UUID -from enum import Enum +from pydantic_core import Url + +class ModelMetadata(BaseModel): + type_name: str = Field() + version: int = Field() class DocumentMixin(BaseModel): uuid: UUID = Field( description="""Unique ID (across the BIA database) used to refer to and identify a document.""" ) - + version: int = Field( + description="""Document version. This can't be optional to make sure we never persist objects without it""" + ) + model: Optional[ModelMetadata] = Field( + description="""Model type and version. Used to map arbitrary objects to a known (possibly previously-used) type. + Optional so we can accept objects without it set and set it on the server. + Can't default_factory because that can't be generated in the api client.""" + ) + model_config = ConfigDict(extra="forbid") + + def __init__(self, *args, **data): + model_version_spec = self.model_config.get("model_version_latest") + + if model_version_spec is None: + raise exceptions.ModelDefinitionInvalid( + f"Class {self.__class__.__name__} missing 'model_version_latest' in its model_config" + ) + model_metadata_expected = ModelMetadata( + type_name=self.__class__.__name__, + version=model_version_spec, + ) + model_metadata_existing = data.get("model", None) + if model_metadata_existing: + if model_metadata_existing != model_metadata_expected: + raise exceptions.UnexpectedDocumentType( + f"Document {str(data.get('uuid'))} has model metadata {model_metadata_existing}, expected : {model_metadata_expected}" + ) + else: + data["model"] = model_metadata_expected.model_dump() + + super().__init__(*args, **data) class UserIdentifiedObject(BaseModel): title_id: str = Field( @@ -23,23 +58,24 @@ class Study( semantic_models.Study, DocumentMixin, ): + experimental_imaging_component: List[UUID] = Field() + annotation_component: List[UUID] = Field() author: List[semantic_models.Contributor] = Field(min_length=1) + description: str = Field() class FileReference( semantic_models.FileReference, DocumentMixin, ): - submission_dataset_uuid: UUID = Field() + submission_dataset: UUID = Field() class ImageRepresentation( semantic_models.ImageRepresentation, DocumentMixin, ): - # We may want to store the FileReference -> Image(Represenation) rather than in the original_file_reference_uuid - original_file_reference_uuid: Optional[List[UUID]] = Field() - representation_of_uuid: UUID = Field() + original_file_reference: Optional[List[UUID]] = Field() class ExperimentalImagingDataset( @@ -47,22 +83,30 @@ class ExperimentalImagingDataset( DocumentMixin, UserIdentifiedObject, ): - submitted_in_study_uuid: UUID = Field() + image: List[UUID] = Field() + file: List[UUID] = Field() + submitted_in_study: UUID = Field() + specimen_preparation_method: List[UUID] = Field() + acquisition_method: List[UUID] = Field() + biological_entity: List[UUID] = Field() + # we include image analysis and correlation -class Specimen(semantic_models.Specimen, DocumentMixin): - imaging_preparation_protocol_uuid: List[UUID] = Field(min_length=1) - sample_of_uuid: List[UUID] = Field(min_length=1) - growth_protocol_uuid: List[UUID] = Field() +class Specimen(semantic_models.Specimen): + preparation_method: List[UUID] = Field(min_length=1) + sample_of: List[UUID] = Field(min_length=1) class ExperimentallyCapturedImage( semantic_models.ExperimentallyCapturedImage, DocumentMixin, ): - acquisition_process_uuid: List[UUID] = Field() - submission_dataset_uuid: UUID = Field() - subject_uuid: UUID = Field() + acquisition_process: List[UUID] = Field() + representation: List[UUID] = Field() + submission_dataset: UUID = Field() + subject: Specimen = Field() + # note Specimen is included in image document, but needs to be overriden to link to protocol & biosample via uuid. + class ImageAcquisition( semantic_models.ImageAcquisition, @@ -72,16 +116,8 @@ class ImageAcquisition( pass -class SpecimenImagingPrepartionProtocol( - semantic_models.SpecimenImagingPrepartionProtocol, - DocumentMixin, - UserIdentifiedObject, -): - pass - - -class SpecimenGrowthProtocol( - semantic_models.SpecimenGrowthProtocol, +class SpecimenPrepartionProtocol( + semantic_models.SpecimenPrepartionProtocol, DocumentMixin, UserIdentifiedObject, ): @@ -101,25 +137,30 @@ class ImageAnnotationDataset( DocumentMixin, UserIdentifiedObject, ): - submitted_in_study_uuid: UUID = Field() + image: List[UUID] = Field() + file: List[UUID] = Field() + annotation_file: List[UUID] = Field() + submitted_in_study: UUID = Field() + annotation_method: List[UUID] = Field() class AnnotationFileReference( semantic_models.AnnotationFileReference, DocumentMixin, ): - submission_dataset_uuid: UUID = Field() - source_image_uuid: List[UUID] = Field() - creation_process_uuid: List[UUID] = Field() + source_image: List[UUID] = Field() + submission_dataset: UUID = Field() + creation_process: List[UUID] = Field() class DerivedImage( semantic_models.DerivedImage, DocumentMixin, ): - source_image_uuid: List[UUID] = Field() - submission_dataset_uuid: UUID = Field() - creation_process_uuid: List[UUID] = Field() + source_image: List[UUID] = Field() + submission_dataset: UUID = Field() + creation_process: List[UUID] = Field() + representation: List[UUID] = Field() class AnnotationMethod( @@ -127,5 +168,4 @@ class AnnotationMethod( DocumentMixin, UserIdentifiedObject, ): - pass - + source_dataset: List[Union[UUID, AnyUrl]] \ No newline at end of file diff --git a/bia-shared-datamodels/src/bia_shared_datamodels/exceptions.py b/bia-shared-datamodels/src/bia_shared_datamodels/exceptions.py new file mode 100644 index 00000000..9bece7a2 --- /dev/null +++ b/bia-shared-datamodels/src/bia_shared_datamodels/exceptions.py @@ -0,0 +1,5 @@ +class UnexpectedDocumentType(Exception): + pass + +class ModelDefinitionInvalid(Exception): + pass \ No newline at end of file