Repository: AllenNeuralDynamics/aind-data-schema

Issue:
Breaking changes in pydantic 2.9 (part 2)
```
======================================================================
FAIL: test_validate_ecephys_metadata (__main__.TestMetadata.test_validate_ecephys_metadata)
Tests that ecephys validator works as expected
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dan/proj/aind/aind-data-schema/tests/test_metadata.py", line 189, in test_validate_ecephys_metadata
    self.assertIn(
AssertionError: 'Missing some metadata for Ecephys. Requires subject, procedures, session, rig, and processing.' not found in "1 validation error for Metadata\n  Value error, Expected 21 fields but got 14 for type `NanojectInjection` with value `NanojectInjection(recovery_time=None, recovery_tim...jection_volume_unit=<VolumeUnit.NL: 'nanoliter'>)` - serialized value may not be as expected. [type=value_error, input_value={'name': 'ecephys_655019_..._axes=None, notes=None)}, input_type=dict]\n    For further information visit https://errors.pydantic.dev/2.9/v/value_error"

======================================================================
FAIL: test_validate_smartspim_metadata (__main__.TestMetadata.test_validate_smartspim_metadata)
Tests that smartspim validator works as expected
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/dan/proj/aind/aind-data-schema/tests/test_metadata.py", line 148, in test_validate_smartspim_metadata
    self.assertIn(
AssertionError: 'Missing some metadata for SmartSpim. Requires subject, procedures, acquisition, and instrument.' not found in "1 validation error for Metadata\n  Value error, Expected 21 fields but got 14 for type `NanojectInjection` with value `NanojectInjection(recovery_time=None, recovery_tim...jection_volume_unit=<VolumeUnit.NL: 'nanoliter'>)` - serialized value may not be as expected. [type=value_error, input_value={'name': 'ecephys_655019_...oftware=[], notes=None)}, input_type=dict]\n    For further information visit https://errors.pydantic.dev/2.9/v/value_error"

```

These remaining issues are due to the use of model_construct() on classes with required fields (that then don't get generated. Something about pydantic 2.9 changed and these are sneaking past our attempt to ignore validation errors. Separating this from the issues Bruno fixed already

Hints from issue comments:
@bruno-f-cruz: A future release may fix this https://github.com/pydantic/pydantic/issues/10344.

If time is short for fixing this may be worth pinning for a couple of weeks.@dbirman: > A future release may fix this [pydantic/pydantic#10344](https://github.com/pydantic/pydantic/issues/10344).
> 
> If time is short for fixing this may be worth pinning for a couple of weeks.

Okay finally had the time to look at what you had posted over there and I finally get the error. Thanks for posting that and getting it fixed (hopefully) upstream.

Another option would be to write a version of model_construct() for ourselves that creates required fields with None. I think it's just a matter of an `else` in this statement https://github.com/pydantic/pydantic/blob/204e109691c69583e656c6e16a62ad79da2f59b9/pydantic/main.py#L291C1-L291C39 that sets the value to None

Solution patch:
diff --git a/examples/quality_control.json b/examples/quality_control.json
index bbd48cf79..317173778 100644
--- a/examples/quality_control.json
+++ b/examples/quality_control.json
@@ -1,8 +1,13 @@
 {
    "describedBy": "https://raw.githubusercontent.com/AllenNeuralDynamics/aind-data-schema/main/src/aind_data_schema/core/quality_control.py",
    "schema_version": "1.0.0",
-   "overall_status": "Pass",
-   "overall_status_date": "2022-11-22",
+   "overall_status": [
+      {
+         "evaluator": "Automated",
+         "status": "Pass",
+         "timestamp": "2022-11-22"
+      }
+   ],
    "evaluations": [
       {
          "evaluation_modality": {
@@ -12,29 +17,33 @@
          "evaluation_stage": "Processing",
          "evaluation_name": "Drift map",
          "evaluation_description": "Qualitative check that drift map shows minimal movement",
-         "evaluator": "Fred Flintstone",
-         "evaluation_date": "2022-11-22",
          "qc_metrics": [
             {
                "name": "Probe A drift",
                "value": "High",
                "description": null,
-               "references": null
+               "reference": "ecephys-drift-map"
             },
             {
                "name": "Probe B drift",
                "value": "Low",
                "description": null,
-               "references": null
+               "reference": "ecephys-drift-map"
             },
             {
                "name": "Probe C drift",
                "value": "Low",
                "description": null,
-               "references": null
+               "reference": "ecephys-drift-map"
+            }
+         ],
+         "evaluation_status": [
+            {
+               "evaluator": "Fred Flintstone",
+               "status": "Fail",
+               "timestamp": "2022-11-22"
             }
          ],
-         "stage_status": "Fail",
          "notes": "Manually annotated: failed due to high drift on probe A"
       },
       {
@@ -45,23 +54,27 @@
          "evaluation_stage": "Raw data",
          "evaluation_name": "Video frame count check",
          "evaluation_description": null,
-         "evaluator": "Fred Flinstone",
-         "evaluation_date": "2022-11-22",
          "qc_metrics": [
             {
                "name": "video_1_num_frames",
                "value": 662,
                "description": null,
-               "references": null
+               "reference": null
             },
             {
                "name": "video_2_num_frames",
                "value": 662,
                "description": null,
-               "references": null
+               "reference": null
+            }
+         ],
+         "evaluation_status": [
+            {
+               "evaluator": "Fred Flintstone",
+               "status": "Fail",
+               "timestamp": "2022-11-22"
             }
          ],
-         "stage_status": "Pass",
          "notes": "Pass when video_1_num_frames==video_2_num_frames"
       },
       {
@@ -72,29 +85,33 @@
          "evaluation_stage": "Raw data",
          "evaluation_name": "Probes present",
          "evaluation_description": null,
-         "evaluator": "Automated",
-         "evaluation_date": "2022-11-22",
          "qc_metrics": [
             {
                "name": "ProbeA_success",
                "value": true,
                "description": null,
-               "references": null
+               "reference": null
             },
             {
                "name": "ProbeB_success",
                "value": true,
                "description": null,
-               "references": null
+               "reference": null
             },
             {
                "name": "ProbeC_success",
                "value": true,
                "description": null,
-               "references": null
+               "reference": null
+            }
+         ],
+         "evaluation_status": [
+            {
+               "evaluator": "Automated",
+               "status": "Pass",
+               "timestamp": "2022-11-22"
             }
          ],
-         "stage_status": "Pass",
          "notes": null
       }
    ],
diff --git a/examples/quality_control.py b/examples/quality_control.py
index 6469d2b87..e63dea24f 100644
--- a/examples/quality_control.py
+++ b/examples/quality_control.py
@@ -4,7 +4,7 @@
 
 from aind_data_schema_models.modalities import Modality
 
-from aind_data_schema.core.quality_control import QCEvaluation, QualityControl, QCMetric, Stage, Status
+from aind_data_schema.core.quality_control import QCEvaluation, QualityControl, QCMetric, Stage, Status, QCStatus
 
 t = date(2022, 11, 22)
 
@@ -13,23 +13,30 @@
     evaluation_description="Qualitative check that drift map shows minimal movement",
     evaluation_modality=Modality.ECEPHYS,
     evaluation_stage=Stage.PROCESSING,
-    evaluator="Fred Flintstone",
-    evaluation_date=t,
+    evaluation_status=[
+        QCStatus(
+            evaluator="Fred Flintstone",
+            timestamp=t,
+            status=Status.FAIL
+        )
+    ],
     qc_metrics=[
         QCMetric(
             name="Probe A drift",
-            value="High"
+            value="High",
+            reference="ecephys-drift-map"
         ),
         QCMetric(
             name="Probe B drift",
-            value="Low"
+            value="Low",
+            reference="ecephys-drift-map"
         ),
         QCMetric(
             name="Probe C drift",
-            value="Low"
+            value="Low",
+            reference="ecephys-drift-map"
         )
     ],
-    stage_status=Status.FAIL,
     notes="Manually annotated: failed due to high drift on probe A"
 )
 
@@ -37,8 +44,13 @@
     evaluation_name="Video frame count check",
     evaluation_modality=Modality.BEHAVIOR_VIDEOS,
     evaluation_stage=Stage.RAW,
-    evaluator="Fred Flinstone",
-    evaluation_date=t,
+    evaluation_status=[
+        QCStatus(
+            evaluator="Fred Flintstone",
+            timestamp=t,
+            status=Status.FAIL
+        )
+    ],
     qc_metrics=[
         QCMetric(
             name="video_1_num_frames",
@@ -49,7 +61,6 @@
             value=662
         )
     ],
-    stage_status=Status.PASS,
     notes="Pass when video_1_num_frames==video_2_num_frames"
 )
 
@@ -57,8 +68,13 @@
     evaluation_name="Probes present",
     evaluation_modality=Modality.ECEPHYS,
     evaluation_stage=Stage.RAW,
-    evaluator="Automated",
-    evaluation_date=t,
+    evaluation_status=[
+        QCStatus(
+            evaluator="Automated",
+            timestamp=t,
+            status=Status.PASS
+        )
+    ],
     qc_metrics=[
         QCMetric(
             name="ProbeA_success",
@@ -73,12 +89,16 @@
             value=True
         )
     ],
-    stage_status=Status.PASS,
 )
 
 q = QualityControl(
-    overall_status="Pass",
-    overall_status_date=t,
+    overall_status=[
+        QCStatus(
+            evaluator="Automated",
+            timestamp=t,
+            status=Status.PASS
+        )
+    ],
     evaluations=[eval0, eval1, eval2]
 )
 
diff --git a/pyproject.toml b/pyproject.toml
index 08708a2a8..c97951092 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -16,7 +16,7 @@ dynamic = ["version"]
 dependencies = [
     'aind-data-schema-models>=0.3.2',
     'dictdiffer',
-    'pydantic>=2.7,<2.9',
+    'pydantic>=2.7',
     'inflection',
     'jsonschema',
     'semver'
@@ -24,7 +24,8 @@ dependencies = [
 
 [project.optional-dependencies]
 dev = [
-    'aind_data_schema[linters]'
+    'aind_data_schema[linters]',
+    'pydantic>=2.7, !=2.9.0, !=2.9.1'
 ]
 
 linters = [
diff --git a/src/aind_data_schema/core/quality_control.py b/src/aind_data_schema/core/quality_control.py
index a6d2b7e28..38224df79 100644
--- a/src/aind_data_schema/core/quality_control.py
+++ b/src/aind_data_schema/core/quality_control.py
@@ -31,12 +31,21 @@ class Stage(str, Enum):
     ANALYSIS = "Analysis"
 
 
+class QCStatus(BaseModel):
+    """Description of a QC status, set by an evaluator"""
+
+    evaluator: str = Field(..., title="Status evaluator full name")
+    status: Status = Field(..., title="Status")
+    timestamp: date = Field(..., title="Status date")
+
+
 class QCMetric(BaseModel):
     """Description of a single quality control metric"""
+
     name: str = Field(..., title="Metric name")
     value: Any = Field(..., title="Metric value")
     description: Optional[str] = Field(default=None, title="Metric description")
-    references: Optional[List[str]] = Field(default=None, title="Metric reference URLs")
+    reference: Optional[str] = Field(default=None, title="Metric reference image URL or plot type")
 
 
 class QCEvaluation(AindModel):
@@ -46,10 +55,8 @@ class QCEvaluation(AindModel):
     evaluation_stage: Stage = Field(..., title="Evaluation stage")
     evaluation_name: str = Field(..., title="Evaluation name")
     evaluation_description: Optional[str] = Field(default=None, title="Evaluation description")
-    evaluator: str = Field(..., title="Evaluator full name")
-    evaluation_date: date = Field(..., title="Evaluation date")
     qc_metrics: List[QCMetric] = Field(..., title="QC metrics")
-    stage_status: Status = Field(..., title="Stage status")
+    evaluation_status: List[QCStatus] = Field(..., title="Evaluation status")
     notes: Optional[str] = Field(default=None, title="Notes")
 
 
@@ -59,7 +66,6 @@ class QualityControl(AindCoreModel):
     _DESCRIBED_BY_URL = AindCoreModel._DESCRIBED_BY_BASE_URL.default + "aind_data_schema/core/quality_control.py"
     describedBy: str = Field(_DESCRIBED_BY_URL, json_schema_extra={"const": _DESCRIBED_BY_URL})
     schema_version: Literal["1.0.0"] = Field("1.0.0")
-    overall_status: Status = Field(..., title="Overall status")
-    overall_status_date: date = Field(..., title="Date of status")
+    overall_status: List[QCStatus] = Field(..., title="Overall status")
     evaluations: List[QCEvaluation] = Field(..., title="Evaluations")
     notes: Optional[str] = Field(default=None, title="Notes")


Test patch:
diff --git a/tests/test_quality_control.py b/tests/test_quality_control.py
index 6e17ffdec..508a5c4bd 100644
--- a/tests/test_quality_control.py
+++ b/tests/test_quality_control.py
@@ -6,7 +6,7 @@
 from aind_data_schema_models.modalities import Modality
 from pydantic import ValidationError
 
-from aind_data_schema.core.quality_control import QCEvaluation, QualityControl, QCMetric, Stage, Status
+from aind_data_schema.core.quality_control import QCEvaluation, QualityControl, QCMetric, Stage, Status, QCStatus
 
 
 class QualityControlTests(unittest.TestCase):
@@ -20,8 +20,13 @@ def test_constructors(self):
 
         test_eval = QCEvaluation(
                 evaluation_name="Drift map",
-                evaluator="Bob",
-                evaluation_date=date.fromisoformat("2020-10-10"),
+                evaluation_status=[
+                    QCStatus(
+                        evaluator="Fred Flintstone",
+                        timestamp=date.fromisoformat("2020-10-10"),
+                        status=Status.PASS
+                    )
+                ],
                 evaluation_modality=Modality.ECEPHYS,
                 evaluation_stage=Stage.PROCESSING,
                 qc_metrics=[
@@ -36,12 +41,16 @@ def test_constructors(self):
                         references=["s3://some-data-somewhere"]
                     )
                 ],
-                stage_status=Status.PASS,
             )
 
         q = QualityControl(
-            overall_status_date=date.fromisoformat("2020-10-10"),
-            overall_status=Status.PASS,
+            overall_status=[
+                QCStatus(
+                    evaluator="Bob",
+                    timestamp=date.fromisoformat("2020-10-10"),
+                    status=Status.PASS
+                )
+            ],
             evaluations=[
                 test_eval
             ],