diff --git a/fairscape_models/activity.py b/fairscape_models/activity.py index 57823ce..a09fae3 100644 --- a/fairscape_models/activity.py +++ b/fairscape_models/activity.py @@ -1,10 +1,10 @@ -from pydantic import BaseModel, Field, ConfigDict, model_validator -from typing import Optional, List, Union +from pydantic import Field, ConfigDict, field_validator +from typing import Optional, List, Union, Any -from fairscape_models.fairscape_base import IdentifierValue +from fairscape_models.fairscape_base import IdentifierValue, Identifier, extractGUID from fairscape_models._version import __version__ -class Activity(BaseModel): +class Activity(Identifier): """Base class for Activity types (Computation, Annotation, Experiment)""" guid: str = Field(alias="@id") name: str @@ -20,3 +20,9 @@ class Activity(BaseModel): fairscapeVersion: str = __version__ model_config = ConfigDict(extra="allow", populate_by_name=True) + +# TODO extract guids from all subfields +# @field_validator('generated', mode='before') +# @classmethod +# def extract_guid_generated(cls, value: Any) -> Any: +# pass \ No newline at end of file diff --git a/fairscape_models/biochem_entity.py b/fairscape_models/biochem_entity.py index 7239a68..b865cbb 100644 --- a/fairscape_models/biochem_entity.py +++ b/fairscape_models/biochem_entity.py @@ -1,10 +1,10 @@ -from pydantic import BaseModel, Field, ConfigDict, model_validator +from pydantic import Field, ConfigDict, model_validator from typing import Optional, List, Union -from fairscape_models.fairscape_base import IdentifierValue, IdentifierPropertyValue +from fairscape_models.fairscape_base import IdentifierValue, IdentifierPropertyValue, Identifier from fairscape_models._version import __version__ -class BioChemEntity(BaseModel): +class BioChemEntity(Identifier): """ Pydantic model for the Schema.org BioChemEntity datatype This class can apply to Protiens, Genes, Chemical Entities, or Biological Samples diff --git a/fairscape_models/digital_object.py b/fairscape_models/digital_object.py index 5a4a4e1..f905def 100644 --- a/fairscape_models/digital_object.py +++ b/fairscape_models/digital_object.py @@ -1,12 +1,12 @@ -from pydantic import BaseModel, Field, ConfigDict, model_validator +from pydantic import Field, ConfigDict, field_validator from typing import Optional, List, Union -from fairscape_models.fairscape_base import IdentifierValue +from fairscape_models.fairscape_base import IdentifierValue, Identifier from fairscape_models._version import __version__ -class DigitalObject(BaseModel): +class DigitalObject(Identifier): """Base class for DigitalObject types (Dataset, Software, MLModel)""" - guid: str = Field(alias="@id") + guid: str = Field(alias="@id", pattern="^ark:[0-9]{5}/.+$") name: str metadataType: Optional[Union[List[str], str]] = Field(default=['prov:Entity', "https://w3id.org/EVI#DigitalObject"], alias="@type") author: Union[str, IdentifierValue, List[Union[str, IdentifierValue]]] @@ -28,3 +28,5 @@ class DigitalObject(BaseModel): wasAttributedTo: Optional[List[Union[str, IdentifierValue]]] = Field(default=[], alias="prov:wasAttributedTo") model_config = ConfigDict(extra="allow", populate_by_name=True) + +# TODO extract guids from all subfields \ No newline at end of file diff --git a/fairscape_models/fairscape_base.py b/fairscape_models/fairscape_base.py index 7084914..faabac0 100644 --- a/fairscape_models/fairscape_base.py +++ b/fairscape_models/fairscape_base.py @@ -2,15 +2,18 @@ BaseModel, ConfigDict, Field, - BeforeValidator + BeforeValidator, + field_validator ) from pydantic.networks import AnyUrl from typing import ( List, Optional, Dict, - Union + Union, + Any ) +import re from typing_extensions import Annotated from enum import Enum @@ -67,6 +70,20 @@ } } +def extractGUID(inputString: str | None) -> str|None: + """ + Given an input ARK extract the normalized ARK, if validation fails return the input. + """ + try: + match = re.search( + pattern="ark:[0-9]{5}/.+$", + string=inputString + ) + return match.group() + except AttributeError: + return inputString + + class ClassType(str, Enum): DATASET = 'Dataset' SOFTWARE = 'Software' @@ -123,16 +140,55 @@ class IdentifierPropertyValue(BaseModel): class Identifier(BaseModel): + """ + The Base Model for any Metadata element in FAIRSCAPE. + + Every instance must have a GUID in the form of an ARK (archival resource key), + a metadata type (https://www.w3.org/TR/json-ld/#specifying-the-type), and a name specified as a string. + Every model must have these attributes, and may have any other attributes as specified by the `ConfigDict(extra='allow')`. + + For the guid property, preprocessing is preformed by the field validator `Identifier.extract_guid`. + This method preforms a regex search to find the identifier within the passed value. + As ARKs may be specified as full IRIS or URLs pointing to several different resolvers, arks are stripped. + The guid for all fairscape_models clases should follow the regex `"ark:[0-9]{5}/.+$"`. + + This guid preprocessing is also preformed on isPartOf. + """ model_config = ConfigDict(extra='allow') guid: str = Field( title="guid", - alias="@id" + alias="@id", + pattern=IdentifierPattern ) - metadataType: ValidatedClassType = Field( + metadataType: Optional[Union[List[str], str]] = Field( title="metadataType", alias="@type" ) name: str = Field(...) + isPartOf: Optional[Union[List[str], str]] = Field(default=[]) + + @field_validator('guid', mode='before') + @classmethod + def extract_guid(cls, value: Any)-> Any: + """ + Extract the ARK from the guid field, runs before validation against regex. + """ + return extractGUID(value) + + + @field_validator('isPartOf', mode='before') + @classmethod + def extract_guid_is_part_of(cls, value: Any)-> Any: + """ + Extract GUID from isPartOf Properties, normalizing the form of the ark. + """ + if value: + if isinstance(value, str): + return extractGUID(value) + if isinstance(value, list): + return [extractGUID(elem) for elem in value] + else: + return value class FairscapeBaseModel(Identifier): diff --git a/fairscape_models/rocrate.py b/fairscape_models/rocrate.py index c4a23fa..b2d3128 100644 --- a/fairscape_models/rocrate.py +++ b/fairscape_models/rocrate.py @@ -430,6 +430,8 @@ def cleanIdentifiers(self): def cleanGUID(metadata): """ Clean metadata guid property from full urls to ark:{NAAN}/{postfix} """ + # TODO should be removed + if hasattr(metadata, 'guid') and isinstance(metadata.guid, str) and "http" in metadata.guid: # old metadata parsing #metadata.guid = urllib.parse.urlparse(metadata.guid).path.lstrip('/')