From 676fc7506046c08479ff26001d6db2630b6015a3 Mon Sep 17 00:00:00 2001 From: Max Levinson Date: Thu, 25 Jun 2026 13:26:55 -0400 Subject: [PATCH 1/5] Adding Regex Validation to Digital Object GUIDs --- fairscape_models/digital_object.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/fairscape_models/digital_object.py b/fairscape_models/digital_object.py index 5a4a4e1..6e1aaa7 100644 --- a/fairscape_models/digital_object.py +++ b/fairscape_models/digital_object.py @@ -1,12 +1,13 @@ -from pydantic import BaseModel, Field, ConfigDict, model_validator -from typing import Optional, List, Union +from pydantic import BaseModel, Field, ConfigDict, model_validator, field_validator +from typing import Optional, List, Union, Any +import re from fairscape_models.fairscape_base import IdentifierValue from fairscape_models._version import __version__ class DigitalObject(BaseModel): """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 +29,18 @@ class DigitalObject(BaseModel): wasAttributedTo: Optional[List[Union[str, IdentifierValue]]] = Field(default=[], alias="prov:wasAttributedTo") model_config = ConfigDict(extra="allow", populate_by_name=True) + + @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 + """ + + try: + match = re.search( + pattern="ark:[0-9]{5}/.+$", + string=value + ) + return match.group() + except AttributeError: + return value From db8e539a96176d8c7cb1a39b02e0f781b942bf92 Mon Sep 17 00:00:00 2001 From: Max Levinson Date: Thu, 25 Jun 2026 16:25:01 -0400 Subject: [PATCH 2/5] Add identifier preprocessing to BaseModels Activity and DigitalObject --- fairscape_models/activity.py | 15 ++++++++++---- fairscape_models/biochem_entity.py | 4 ++-- fairscape_models/digital_object.py | 24 +++++----------------- fairscape_models/fairscape_base.py | 33 ++++++++++++++++++++++++++---- 4 files changed, 47 insertions(+), 29 deletions(-) diff --git a/fairscape_models/activity.py b/fairscape_models/activity.py index 57823ce..51b0c5f 100644 --- a/fairscape_models/activity.py +++ b/fairscape_models/activity.py @@ -1,10 +1,11 @@ -from pydantic import BaseModel, Field, ConfigDict, model_validator -from typing import Optional, List, Union +from pydantic import BaseModel, Field, ConfigDict, model_validator, field_validator +from typing import Optional, List, Union, Any +import re -from fairscape_models.fairscape_base import IdentifierValue +from fairscape_models.fairscape_base import IdentifierValue, Identifier 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 +21,9 @@ class Activity(BaseModel): fairscapeVersion: str = __version__ model_config = ConfigDict(extra="allow", populate_by_name=True) + + @field_validator('generated', mode='before') + @classmethod + def extract_guid_generated(): + pass + \ No newline at end of file diff --git a/fairscape_models/biochem_entity.py b/fairscape_models/biochem_entity.py index 7239a68..26ede71 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 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 6e1aaa7..f905def 100644 --- a/fairscape_models/digital_object.py +++ b/fairscape_models/digital_object.py @@ -1,11 +1,10 @@ -from pydantic import BaseModel, Field, ConfigDict, model_validator, field_validator -from typing import Optional, List, Union, Any -import re +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", pattern="^ark:[0-9]{5}/.+$") name: str @@ -30,17 +29,4 @@ class DigitalObject(BaseModel): model_config = ConfigDict(extra="allow", populate_by_name=True) - @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 - """ - - try: - match = re.search( - pattern="ark:[0-9]{5}/.+$", - string=value - ) - return match.group() - except AttributeError: - return value +# 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..b85419b 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 @@ -126,14 +129,36 @@ class Identifier(BaseModel): 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(...) + @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 + """ + + try: + match = re.search( + pattern="ark:[0-9]{5}/.+$", + string=value + ) + return match.group() + except AttributeError: + return value + + # TODO extract guids from isPartOf + # @field_validator('generated', mode='before') + # @classmethod + # def extract_guid_is_part_of(): + # pass + class FairscapeBaseModel(Identifier): """Refers to the Fairscape BaseModel inherited from Pydantic From fd912eac239c92592a137862769a15b094eeecec Mon Sep 17 00:00:00 2001 From: Max Levinson Date: Tue, 30 Jun 2026 12:45:38 -0400 Subject: [PATCH 3/5] fairscape_base added docstring, added validation to isPartOf --- fairscape_models/fairscape_base.py | 59 +++++++++++++++++++++++------- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/fairscape_models/fairscape_base.py b/fairscape_models/fairscape_base.py index b85419b..faabac0 100644 --- a/fairscape_models/fairscape_base.py +++ b/fairscape_models/fairscape_base.py @@ -70,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' @@ -126,6 +140,20 @@ 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", @@ -137,27 +165,30 @@ class Identifier(BaseModel): 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 + """ + Extract the ARK from the guid field, runs before validation against regex. """ + return extractGUID(value) + - try: - match = re.search( - pattern="ark:[0-9]{5}/.+$", - string=value - ) - return match.group() - except AttributeError: + @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 - - # TODO extract guids from isPartOf - # @field_validator('generated', mode='before') - # @classmethod - # def extract_guid_is_part_of(): - # pass class FairscapeBaseModel(Identifier): From 9691d967a407ae7c10a62d27b70a603bcf3a6c5a Mon Sep 17 00:00:00 2001 From: Max Levinson Date: Tue, 30 Jun 2026 12:45:54 -0400 Subject: [PATCH 4/5] Added TODOs for identifierCleaning --- fairscape_models/activity.py | 9 +++++---- fairscape_models/rocrate.py | 2 ++ 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/fairscape_models/activity.py b/fairscape_models/activity.py index 51b0c5f..daa10da 100644 --- a/fairscape_models/activity.py +++ b/fairscape_models/activity.py @@ -22,8 +22,9 @@ class Activity(Identifier): model_config = ConfigDict(extra="allow", populate_by_name=True) - @field_validator('generated', mode='before') - @classmethod - def extract_guid_generated(): - pass +# TODO extract guids from all subfields +# @field_validator('generated', mode='before') +# @classmethod +# def extract_guid_generated(): +# pass \ No newline at end of file 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('/') From ccfbd1f3fc794ddb044f1f6ad7f8a738ecd817e0 Mon Sep 17 00:00:00 2001 From: Max Levinson Date: Tue, 30 Jun 2026 13:29:55 -0400 Subject: [PATCH 5/5] Removing unused imports --- fairscape_models/activity.py | 10 ++++------ fairscape_models/biochem_entity.py | 2 +- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/fairscape_models/activity.py b/fairscape_models/activity.py index daa10da..a09fae3 100644 --- a/fairscape_models/activity.py +++ b/fairscape_models/activity.py @@ -1,8 +1,7 @@ -from pydantic import BaseModel, Field, ConfigDict, model_validator, field_validator +from pydantic import Field, ConfigDict, field_validator from typing import Optional, List, Union, Any -import re -from fairscape_models.fairscape_base import IdentifierValue, Identifier +from fairscape_models.fairscape_base import IdentifierValue, Identifier, extractGUID from fairscape_models._version import __version__ class Activity(Identifier): @@ -25,6 +24,5 @@ class Activity(Identifier): # TODO extract guids from all subfields # @field_validator('generated', mode='before') # @classmethod -# def extract_guid_generated(): -# pass - \ No newline at end of file +# 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 26ede71..b865cbb 100644 --- a/fairscape_models/biochem_entity.py +++ b/fairscape_models/biochem_entity.py @@ -1,4 +1,4 @@ -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, Identifier