From f9cd3c10b6e5c2313615b2d1e1fe22b8e46ed224 Mon Sep 17 00:00:00 2001 From: Liviu Anita Date: Wed, 31 Jul 2024 11:31:21 +0100 Subject: [PATCH] Add clean version of model version and object version to the models in version control --- .../bia_shared_datamodels/bia_data_model.py | 72 ++++++++++++++++--- .../src/bia_shared_datamodels/exceptions.py | 5 ++ 2 files changed, 66 insertions(+), 11 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 48f785d0..becd236e 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,23 +1,55 @@ from __future__ import annotations -from . import semantic_models +from . import semantic_models, exceptions from pydantic import BaseModel, Field, ConfigDict from typing import List, Optional from uuid import UUID from enum import Enum +class ModelMetadata(BaseModel): + type_name: str = Field() + version: int = Field() class DocumentMixin(BaseModel): - - # Throw error if you try to validate/create model from a dictionary with keys that aren't a field in the model - model_config = ConfigDict(extra="forbid") - 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 because for some usecases (e.g. api) we want to accept objects without it because we have the info we need to set it.""" + ) + + # Throw error if you try to validate/create model from a dictionary with keys that aren't a field in the model + 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( @@ -31,6 +63,8 @@ class Study( ): author: List[semantic_models.Contributor] = Field(min_length=1) + model_config = ConfigDict(model_version_latest=1) + class FileReference( semantic_models.FileReference, @@ -38,6 +72,8 @@ class FileReference( ): submission_dataset_uuid: UUID = Field() + model_config = ConfigDict(model_version_latest=1) + class ImageRepresentation( semantic_models.ImageRepresentation, @@ -47,6 +83,8 @@ class ImageRepresentation( original_file_reference_uuid: Optional[List[UUID]] = Field() representation_of_uuid: UUID = Field() + model_config = ConfigDict(model_version_latest=1) + class ExperimentalImagingDataset( semantic_models.ExperimentalImagingDataset, @@ -55,12 +93,16 @@ class ExperimentalImagingDataset( ): submitted_in_study_uuid: UUID = Field() + model_config = ConfigDict(model_version_latest=1) + 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() + model_config = ConfigDict(model_version_latest=1) + class ExperimentallyCapturedImage( semantic_models.ExperimentallyCapturedImage, @@ -70,12 +112,15 @@ class ExperimentallyCapturedImage( submission_dataset_uuid: UUID = Field() subject_uuid: UUID = Field() + model_config = ConfigDict(model_version_latest=1) + + class ImageAcquisition( semantic_models.ImageAcquisition, DocumentMixin, UserIdentifiedObject, ): - pass + model_config = ConfigDict(model_version_latest=1) class SpecimenImagingPrepartionProtocol( @@ -83,7 +128,7 @@ class SpecimenImagingPrepartionProtocol( DocumentMixin, UserIdentifiedObject, ): - pass + model_config = ConfigDict(model_version_latest=1) class SpecimenGrowthProtocol( @@ -91,7 +136,7 @@ class SpecimenGrowthProtocol( DocumentMixin, UserIdentifiedObject, ): - pass + model_config = ConfigDict(model_version_latest=1) class BioSample( @@ -99,7 +144,7 @@ class BioSample( DocumentMixin, UserIdentifiedObject, ): - pass + model_config = ConfigDict(model_version_latest=1) class ImageAnnotationDataset( @@ -109,6 +154,8 @@ class ImageAnnotationDataset( ): submitted_in_study_uuid: UUID = Field() + model_config = ConfigDict(model_version_latest=1) + class AnnotationFileReference( semantic_models.AnnotationFileReference, @@ -118,6 +165,8 @@ class AnnotationFileReference( source_image_uuid: List[UUID] = Field() creation_process_uuid: List[UUID] = Field() + model_config = ConfigDict(model_version_latest=1) + class DerivedImage( semantic_models.DerivedImage, @@ -127,11 +176,12 @@ class DerivedImage( submission_dataset_uuid: UUID = Field() creation_process_uuid: List[UUID] = Field() + model_config = ConfigDict(model_version_latest=1) + class AnnotationMethod( semantic_models.AnnotationMethod, DocumentMixin, UserIdentifiedObject, ): - pass - + model_config = ConfigDict(model_version_latest=1) 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..e8858b1c --- /dev/null +++ b/bia-shared-datamodels/src/bia_shared_datamodels/exceptions.py @@ -0,0 +1,5 @@ +class UnexpectedDocumentType(Exception): + pass + +class ModelDefinitionInvalid(Exception): + pass