Skip to content

Commit 2ec8648

Browse files
Add git hash to Code model (#1813)
* Add git hash to Code model * update docs * Increase hash length upper bound * Change property name and emit warning if hash and version are not defined * Refactor type alias * update docs * Break lines * Update tests to reflect longer hashes --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com>
1 parent f0154a2 commit 2ec8648

3 files changed

Lines changed: 58 additions & 2 deletions

File tree

docs/source/components/identifiers.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ Code or script identifier
1111
| `url` | `str` | Code URL (URL to code repository) |
1212
| `name` | `Optional[str]` | Name |
1313
| `version` | `Optional[str]` | Code version |
14+
| `commit_hash` | `Optional[str]` | Commit hash (Commit hash of the code.) |
1415
| `container` | Optional[[Container](#container)] | Container |
1516
| `run_script` | `Optional[pathlib.Path]` | Run script (Path to run script) |
1617
| `language` | `Optional[str]` | Programming language (Programming language used) |

src/aind_data_schema/components/identifiers.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@
55
from typing import Dict, List, Optional
66

77
from aind_data_schema_models.registries import Registry
8-
from pydantic import Field
8+
from pydantic import Field, model_validator
9+
import warnings
910

11+
from typing import Annotated
12+
from pydantic import StringConstraints
1013
from aind_data_schema.base import DataModel, DiscriminatedList, GenericModel
1114

1215

@@ -67,12 +70,22 @@ class Container(DataModel):
6770
uri: str = Field(..., title="URI", description="URI of the container, e.g. Docker Hub URL")
6871

6972

73+
CommitHash = Annotated[
74+
str,
75+
StringConstraints(
76+
pattern=r"^[0-9a-fA-F]{7,60}$",
77+
strip_whitespace=True,
78+
),
79+
]
80+
81+
7082
class Code(DataModel):
7183
"""Code or script identifier"""
7284

7385
url: str = Field(..., title="Code URL", description="URL to code repository")
7486
name: Optional[str] = Field(default=None, title="Name")
7587
version: Optional[str] = Field(default=None, title="Code version")
88+
commit_hash: Optional[CommitHash] = Field(default=None, title="Commit hash", description="Commit hash of the code.")
7689

7790
container: Optional[Container] = Field(default=None, title="Container")
7891
run_script: Optional[Path] = Field(default=None, title="Run script", description="Path to run script")
@@ -92,3 +105,14 @@ class Code(DataModel):
92105
title="Core dependency",
93106
description="For code with a core software package dependency, e.g. Bonsai",
94107
)
108+
109+
@model_validator(mode="after")
110+
def _ensure_commit_hash_or_version(self) -> "Code":
111+
"""Ensure that at least one of commit_hash or version is provided for code identification"""
112+
if not self.commit_hash and not self.version:
113+
warnings.warn(
114+
"Neither commit_hash nor version provided for Code. "
115+
"It's recommended to provide at least one to ensure reproducibility. "
116+
"In the future, we will require at least one of these fields."
117+
)
118+
return self

tests/test_identifiers.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
from pydantic import ValidationError
66

7-
from aind_data_schema.components.identifiers import Person
7+
from aind_data_schema.components.identifiers import Code, Person
88

99

1010
class Testexperimenter(unittest.TestCase):
@@ -23,5 +23,36 @@ def test_experimenter_missing_fields(self):
2323
Person()
2424

2525

26+
class TestGitHash(unittest.TestCase):
27+
"""Test GitHash type validation via Code model"""
28+
29+
def test_git_hash_valid(self):
30+
"""Valid git hashes are accepted and stored correctly"""
31+
cases = [
32+
("abc1234", "abc1234"),
33+
("a" * 40, "a" * 40),
34+
("a" * 60, "a" * 60),
35+
("aBcDeF1", "aBcDeF1"),
36+
("deadbeef1234", "deadbeef1234"),
37+
(" abc1234 ", "abc1234"), # strip_whitespace=True strips before validation
38+
]
39+
for git_hash, expected in cases:
40+
with self.subTest(git_hash=git_hash):
41+
code = Code(url="https://github.com/org/repo", commit_hash=git_hash)
42+
self.assertEqual(code.commit_hash, expected)
43+
44+
def test_git_hash_invalid(self):
45+
"""Invalid git hashes raise ValidationError"""
46+
cases = [
47+
"abc123", # too short (6 chars)
48+
"a" * 61, # too long (61 chars)
49+
"xyz12345", # non-hex characters
50+
]
51+
for git_hash in cases:
52+
with self.subTest(git_hash=git_hash):
53+
with self.assertRaises(ValidationError):
54+
Code(url="https://github.com/org/repo", commit_hash=git_hash)
55+
56+
2657
if __name__ == "__main__":
2758
unittest.main()

0 commit comments

Comments
 (0)