Skip to content
Closed
2 changes: 1 addition & 1 deletion .github/workflows/publish_dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@ jobs:
- name: Create and upload schemas
run: |
uv sync --locked
uv run python -m biodata_schema.utils.json_writer --output $TEMP_DIR --attach-version
uv run python -m biodata_schema.utils.json_writer --output $TEMP_DIR --attach-version --draft
uvx --from awscli aws s3 sync $TEMP_DIR s3://${AWS_DATA_SCHEMA_BUCKET}/$S3_PREFIX
2 changes: 1 addition & 1 deletion .github/workflows/tag_and_publish_main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,5 +66,5 @@ jobs:
- name: Create and upload schemas
run: |
uv sync --locked
uv run python -m biodata_schema.utils.json_writer --output $TEMP_DIR --attach-version
uv run python -m biodata_schema.utils.json_writer --output $TEMP_DIR --attach-version --draft
uvx --from awscli aws s3 sync $TEMP_DIR s3://${AWS_DATA_SCHEMA_BUCKET}/$S3_PREFIX
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ readme = "README.md"
dynamic = ["version"]

dependencies = [
'biodata-models>=1.0.1,<2',
'biodata-models>=2.0.1,<3',
'pydantic>=2.7, <3',
'pydantic-extra-types',
'tzdata',
Expand All @@ -25,11 +25,12 @@ dev = [
'argparse',
'codespell',
'dictdiffer',
'jsonschema>=4.23,<5',
'pydantic>=2.7, !=2.9.0, !=2.9.1',
'pytest>=8.3',
'pytest-cov>=6',
'ruff>=0.11',
'semver',
'semver>=3.0.0',
]

docs = [
Expand Down
2 changes: 1 addition & 1 deletion src/biodata_schema/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
"""base module for biodata-schema"""

__version__ = "3.0.0"
__version__ = "4.0.0"
4 changes: 4 additions & 0 deletions src/biodata_schema/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
MAX_FILE_SIZE = 500 * 1024 # 500KB


class DraftRequirement:
"""Marker for fields required by the draft metadata schema."""


def _coerce_naive_datetime(v: Any, handler: ValidatorFunctionWrapHandler) -> AwareDatetime:
"""Validator to wrap around AwareDatetime to set a default timezone as user's locale"""
try:
Expand Down
14 changes: 14 additions & 0 deletions src/biodata_schema/components/subjects.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from enum import Enum
from typing import Annotated, List, Optional

from biodata_models.cell_line import CellLineModel
from biodata_models.organizations import Organization
from biodata_models.pid_names import PIDName
from biodata_models.species import Species, Strain
Expand Down Expand Up @@ -195,3 +196,16 @@ class CalibrationObject(DataModel):
objects: Optional[list[Device]] = Field(
default=None, title="Objects", description="For calibration objects that are built up from one or more devices."
)


class CellLine(DataModel):
"""Description of a cultured cell line"""

cell_line_name: str = Field(..., title="Cell line name")
cell_line_type: CellLineModel = Field(..., title="Cell line type")
species: Species.ONE_OF = Field(..., title="Species")
protein: PIDName = Field(..., title="Protein labeled", description="Protein uses UniProt registry")
gene: PIDName = Field(..., title="Gene targeted", description="Gene uses NCBI taxonomy")
cell_structure: str = Field(..., title="Cell structure protein found in") # TODO: ontology or enum in model?
fluorescent_protein: PIDName = Field(..., title="Fluorescent protein", description="Uses FPbase")
clone_number: Optional[int] = Field(default=None, title="Clone number")
3 changes: 2 additions & 1 deletion src/biodata_schema/core/acquisition.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
DataCoreModel,
DataModel,
DiscriminatedList,
DraftRequirement,
GenericModel,
)
from biodata_schema.components.configs import (
Expand Down Expand Up @@ -389,7 +390,7 @@ class Acquisition(ProtocolListMixin, DataCoreModel):
)

# Acquisition metadata
acquisition_start_time: AwareDatetimeWithDefault = Field(
acquisition_start_time: Annotated[AwareDatetimeWithDefault, DraftRequirement] = Field(
...,
title="Acquisition start time",
description="During validation, timezone information will be moved into the acquisition_start_tz field.",
Expand Down
8 changes: 4 additions & 4 deletions src/biodata_schema/core/data_description.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""Generic metadata classes for data"""

import re
from typing import List, Literal, Optional
from typing import Annotated, List, Literal, Optional

from biodata_models.data_name_patterns import (
DataLevel,
Expand All @@ -15,7 +15,7 @@
from biodata_models.organizations import Organization
from pydantic import Field, SkipValidation, model_validator

from biodata_schema.base import AwareDatetimeWithDefault, DataCoreModel, DataModel
from biodata_schema.base import AwareDatetimeWithDefault, DataCoreModel, DataModel, DraftRequirement
from biodata_schema.components.identifiers import Person


Expand All @@ -35,7 +35,7 @@ class DataDescription(DataCoreModel):
_DESCRIBED_BY_URL = DataCoreModel._DESCRIBED_BY_BASE_URL.default + "biodata_schema/core/data_description.py"
describedBy: str = Field(default=_DESCRIBED_BY_URL, json_schema_extra={"const": _DESCRIBED_BY_URL})
schema_version: SkipValidation[Literal["3.0.0"]] = Field(default="3.0.0")
license: License = Field(default=License.CC_BY_40, title="License")
license: Annotated[License, DraftRequirement] = Field(default=License.CC_BY_40, title="License")

subject_id: Optional[str] = Field(
default=None,
Expand Down Expand Up @@ -89,7 +89,7 @@ class DataDescription(DataCoreModel):
title="Investigators",
min_length=1,
)
project_name: str = Field(
project_name: Annotated[str, DraftRequirement] = Field(
...,
pattern=DataRegex.NO_SPECIAL_CHARS_EXCEPT_SPACE.value,
description="A name for a set of coordinated activities intended to achieve one or more objectives.",
Expand Down
6 changes: 3 additions & 3 deletions src/biodata_schema/core/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@

import logging
from datetime import date
from typing import List, Literal, Optional
from typing import Annotated, List, Literal, Optional

from biodata_models.modalities import Modality
from pydantic import Field, SkipValidation, field_validator, model_validator

from biodata_schema.base import DataCoreModel, DiscriminatedList
from biodata_schema.base import DataCoreModel, DiscriminatedList, DraftRequirement
from biodata_schema.components.connections import Connection
from biodata_schema.components.coordinates import CoordinateSystem
from biodata_schema.components.devices import (
Expand Down Expand Up @@ -82,7 +82,7 @@ class Instrument(DataCoreModel):

# instrument definition
location: Optional[str] = Field(default=None, title="Location", description="Location of the instrument")
instrument_id: str = Field(
instrument_id: Annotated[str, DraftRequirement] = Field(
...,
description="Unique instrument identifier",
title="Instrument ID",
Expand Down
31 changes: 28 additions & 3 deletions src/biodata_schema/core/metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import logging
import warnings
from pathlib import Path
from typing import Dict, List, Literal, Optional, Union, get_args
from typing import Annotated, Dict, List, Literal, Optional, Union, get_args

from biodata_models.modalities import Modality
from pydantic import (
Expand All @@ -19,7 +19,7 @@
model_validator,
)

from biodata_schema.base import DataCoreModel
from biodata_schema.base import DataCoreModel, DraftRequirement
from biodata_schema.components.identifiers import DatabaseIdentifiers
from biodata_schema.components.subject_procedures import TrainingProtocol
from biodata_schema.components.subjects import CalibrationObject
Expand Down Expand Up @@ -80,7 +80,7 @@ class Metadata(DataCoreModel):
description="Name of the data asset.",
title="Data Asset Name",
)
location: str = Field(
location: Annotated[str, DraftRequirement] = Field(
...,
title="Location",
description="Current location of the data asset.",
Expand Down Expand Up @@ -296,6 +296,31 @@ def validate_calibration_object_tags(self):

return self

@model_validator(mode="after")
def validate_subject_id_consistency(self):
"""Validator to ensure procedures, acquisition, and data_description subject_id match subject.subject_id"""

if not self.subject:
return self

expected = self.subject.subject_id
mismatches = []
# getattr guards against objects built with model_construct() that omit subject_id
procedures_subject_id = getattr(self.procedures, "subject_id", None)
if procedures_subject_id is not None and procedures_subject_id != expected:
mismatches.append(f"procedures.subject_id={procedures_subject_id}")
acquisition_subject_id = getattr(self.acquisition, "subject_id", None)
if acquisition_subject_id is not None and acquisition_subject_id != expected:
mismatches.append(f"acquisition.subject_id={acquisition_subject_id}")
data_description_subject_id = getattr(self.data_description, "subject_id", None)
if data_description_subject_id is not None and data_description_subject_id != expected:
mismatches.append(f"data_description.subject_id={data_description_subject_id}")

if mismatches:
raise ValueError(f"subject_id mismatch with subject.subject_id={expected}: {', '.join(mismatches)}")

return self

@model_validator(mode="after")
def validate_training_protocol_references(self):
"""Validate that training_protocol_name in StimulusEpoch matches a TrainingProtocol in procedures"""
Expand Down
22 changes: 14 additions & 8 deletions src/biodata_schema/core/subject.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
"""schema for mostly mouse metadata"""
"""schema for subject metadata"""

from typing import Literal, Optional
from typing import Annotated, Literal, Optional

from pydantic import Field, SkipValidation

from biodata_schema.base import DataCoreModel, Discriminated
from biodata_schema.components.subjects import CalibrationObject, HumanSubject, MouseSubject, NonHumanPrimateSubject
from biodata_schema.base import DataCoreModel, Discriminated, DraftRequirement
from biodata_schema.components.subjects import (
CalibrationObject,
CellLine,
HumanSubject,
MouseSubject,
NonHumanPrimateSubject,
)


class Subject(DataCoreModel):
Expand All @@ -14,14 +20,14 @@ class Subject(DataCoreModel):
_DESCRIBED_BY_URL = DataCoreModel._DESCRIBED_BY_BASE_URL.default + "biodata_schema/core/subject.py"
describedBy: str = Field(default=_DESCRIBED_BY_URL, json_schema_extra={"const": _DESCRIBED_BY_URL})
schema_version: SkipValidation[Literal["3.0.2"]] = Field(default="3.0.2")
subject_id: str = Field(
subject_id: Annotated[str, DraftRequirement] = Field(
...,
description="Unique identifier for the subject of data acquisition",
title="Subject ID",
)

subject_details: Discriminated[MouseSubject | HumanSubject | NonHumanPrimateSubject | CalibrationObject] = Field(
..., title="Subject Details"
)
subject_details: Discriminated[
MouseSubject | HumanSubject | NonHumanPrimateSubject | CellLine | CalibrationObject
] = Field(..., title="Subject Details")

notes: Optional[str] = Field(default=None, title="Notes")
Loading
Loading