Skip to content

Commit b277a17

Browse files
authored
fix: checks for existing processing, merges models (#436)
* fix: checks for existing processing, merges models * raise error when merge fails, load of existing processing fails * linter fix * linter fix (in tests)
1 parent 849920d commit b277a17

4 files changed

Lines changed: 154 additions & 12 deletions

File tree

src/aind_metadata_mapper/gather_metadata.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -313,7 +313,7 @@ def _run_mappers_for_acquisition(self):
313313
job_settings = MapperJobSettings(
314314
input_filepath=Path(input_path),
315315
output_directory=Path(input_dir),
316-
output_filename_suffix=mapper_name
316+
output_filename_suffix=mapper_name,
317317
)
318318
try:
319319
mapper = mapper_cls()

src/aind_metadata_mapper/gather_processing_job.py

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Entrypoint to create a processing.json file"""
22

33
import json
4+
import logging
45
import sys
56
from pathlib import Path
67

@@ -30,12 +31,42 @@ def __init__(self, settings: JobSettings):
3031
"""Class constructor"""
3132
self.settings = settings
3233

34+
def load_existing_processing(self) -> Processing | None:
35+
"""Load an existing processing.json file from the output directory, if it exists."""
36+
file_name = Processing.default_filename()
37+
existing_path = Path(self.settings.output_directory) / file_name
38+
if existing_path.exists():
39+
try:
40+
with open(existing_path, "r") as f:
41+
existing_data = json.load(f)
42+
return Processing(**existing_data)
43+
except Exception as e:
44+
error_msg = "Failed to load existing processing.json: {e}. "
45+
logging.error(error_msg)
46+
raise e
47+
return None
48+
3349
def run_job(self):
3450
"""Write the processing object to a directory."""
3551

52+
existing_processing = self.load_existing_processing()
53+
if existing_processing is not None:
54+
try:
55+
merged_processing = existing_processing + self.settings.processing
56+
except Exception as e:
57+
error_msg = (
58+
f"Failed to merge existing processing.json with new processing data: {e}. "
59+
"Cannot proceed without risking data loss. Please verify the existing processing.json "
60+
"file is valid and compatible with the new processing data."
61+
)
62+
logging.error(error_msg)
63+
raise e
64+
else:
65+
merged_processing = self.settings.processing
66+
3667
file_name = Processing.default_filename()
3768
output_path = Path(self.settings.output_directory) / file_name
38-
json_contents = self.settings.processing.model_dump(mode="json", exclude_none=True)
69+
json_contents = merged_processing.model_dump(mode="json", exclude_none=True)
3970
with open(output_path, "w") as f:
4071
json.dump(json_contents, f, indent=3, ensure_ascii=False, sort_keys=True)
4172

tests/test_base.py

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,7 @@ def test_jobsettings_fields(self):
2626
output_dir = Path("/tmp")
2727
output_suffix = "_test"
2828
settings = MapperJobSettings(
29-
input_filepath=input_fp,
30-
output_directory=output_dir,
31-
output_filename_suffix=output_suffix
29+
input_filepath=input_fp, output_directory=output_dir, output_filename_suffix=output_suffix
3230
)
3331
self.assertEqual(settings.input_filepath, input_fp)
3432
self.assertEqual(settings.output_directory, output_dir)
@@ -37,9 +35,7 @@ def test_jobsettings_fields(self):
3735
def test_mapperjob_not_implemented(self):
3836
"""Test that MapperJob raises NotImplementedError"""
3937
settings = MapperJobSettings(
40-
input_filepath=Path("/tmp/in.json"),
41-
output_directory=Path("/tmp"),
42-
output_filename_suffix="_test"
38+
input_filepath=Path("/tmp/in.json"), output_directory=Path("/tmp"), output_filename_suffix="_test"
4339
)
4440
mapper = MapperJob()
4541
with self.assertRaises(NotImplementedError):
@@ -51,9 +47,7 @@ def test_mapperjob_subclass(self):
5147
output_dir = Path("/tmp")
5248
output_suffix = "_test"
5349
settings = MapperJobSettings(
54-
input_filepath=input_fp,
55-
output_directory=output_dir,
56-
output_filename_suffix=output_suffix
50+
input_filepath=input_fp, output_directory=output_dir, output_filename_suffix=output_suffix
5751
)
5852
mapper = DummyMapper()
5953
mapper.was_run = False

tests/test_gather_processing_job.py

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,78 @@
2323
class TestGatherProcessingJob(unittest.TestCase):
2424
"""Tests methods in GatherProcessingJob class."""
2525

26+
@patch("pathlib.Path.exists")
27+
@patch("builtins.open", new_callable=mock_open, read_data='{"data_processes": []}')
28+
@patch("json.load")
29+
def test_load_existing_processing_exists_and_valid(
30+
self, mock_json_load: MagicMock, mock_file_open: MagicMock, mock_exists: MagicMock
31+
):
32+
"""Tests load_existing_processing when file exists and is valid."""
33+
mock_exists.return_value = True
34+
existing_processing_dict = {
35+
"data_processes": [
36+
{
37+
"start_date_time": "2024-09-09T01:02:03",
38+
"end_date_time": "2024-09-10T01:02:03",
39+
"process_type": "Image atlas alignment",
40+
"experimenters": ["Existing Experimenter"],
41+
"stage": "Processing",
42+
"code": {"url": "www.example.com/existing"},
43+
}
44+
]
45+
}
46+
mock_json_load.return_value = existing_processing_dict
47+
48+
example_processing = Processing(data_processes=[])
49+
example_settings = JobSettings(output_directory="example", processing=example_processing)
50+
job = GatherProcessingJob(settings=example_settings)
51+
52+
result = job.load_existing_processing()
53+
54+
self.assertIsNotNone(result)
55+
self.assertIsInstance(result, Processing)
56+
self.assertEqual(len(result.data_processes), 1)
57+
mock_exists.assert_called_once()
58+
mock_json_load.assert_called_once()
59+
60+
@patch("pathlib.Path.exists")
61+
def test_load_existing_processing_does_not_exist(self, mock_exists: MagicMock):
62+
"""Tests load_existing_processing when file does not exist."""
63+
mock_exists.return_value = False
64+
65+
example_processing = Processing(data_processes=[])
66+
example_settings = JobSettings(output_directory="example", processing=example_processing)
67+
job = GatherProcessingJob(settings=example_settings)
68+
69+
result = job.load_existing_processing()
70+
71+
self.assertIsNone(result)
72+
mock_exists.assert_called_once()
73+
74+
@patch("pathlib.Path.exists")
75+
@patch("builtins.open", new_callable=mock_open)
76+
@patch("json.load")
77+
@patch("logging.error")
78+
def test_load_existing_processing_invalid_schema(
79+
self, mock_log_error: MagicMock, mock_json_load: MagicMock, mock_file_open: MagicMock, mock_exists: MagicMock
80+
):
81+
"""Tests load_existing_processing when file exists but doesn't match Processing schema."""
82+
mock_exists.return_value = True
83+
mock_json_load.return_value = {"invalid_field": "value"}
84+
85+
example_processing = Processing(data_processes=[])
86+
example_settings = JobSettings(output_directory="example", processing=example_processing)
87+
job = GatherProcessingJob(settings=example_settings)
88+
89+
with self.assertRaises(Exception):
90+
job.load_existing_processing()
91+
92+
mock_log_error.assert_called_once()
93+
2694
@patch("builtins.open", new_callable=mock_open)
2795
@patch("json.dump")
2896
def test_run_job(self, mock_json_dump: MagicMock, mock_file_open: MagicMock):
29-
"""Tests run_job method."""
97+
"""Tests run_job method with example processing data."""
3098
# noinspection PyArgumentList
3199
example_processing = Processing(
32100
data_processes=[
@@ -58,6 +126,55 @@ def test_run_job(self, mock_json_dump: MagicMock, mock_file_open: MagicMock):
58126
mock_file_open.assert_has_calls(calls=[call(Path("example") / "processing.json", "w")])
59127
mock_json_dump.assert_called_once()
60128

129+
@patch("aind_metadata_mapper.gather_processing_job.GatherProcessingJob.load_existing_processing")
130+
@patch("builtins.open", new_callable=mock_open)
131+
@patch("json.dump")
132+
@patch("logging.error")
133+
def test_run_job_merge_fails_raises_error(
134+
self,
135+
mock_log_error: MagicMock,
136+
mock_json_dump: MagicMock,
137+
mock_file_open: MagicMock,
138+
mock_load_existing: MagicMock,
139+
):
140+
"""Tests run_job raises exception when merge fails."""
141+
existing_processing = Processing(
142+
data_processes=[
143+
DataProcess(
144+
start_date_time=datetime(2024, 9, 9, 1, 2, 3),
145+
end_date_time=datetime(2024, 9, 10, 1, 2, 3),
146+
process_type=ProcessName.IMAGE_ATLAS_ALIGNMENT,
147+
experimenters=["Existing Experimenter"],
148+
stage=ProcessStage.PROCESSING,
149+
code=Code(url="www.example.com/existing_process"),
150+
)
151+
]
152+
)
153+
mock_load_existing.return_value = existing_processing
154+
with patch.object(Processing, "__add__", side_effect=Exception("Merge failed")):
155+
new_processing = Processing(
156+
data_processes=[
157+
DataProcess(
158+
start_date_time=datetime(2024, 10, 10, 1, 2, 3),
159+
end_date_time=datetime(2024, 10, 11, 1, 2, 3),
160+
process_type=ProcessName.COMPRESSION,
161+
experimenters=["AIND Scientific Computing"],
162+
stage=ProcessStage.PROCESSING,
163+
code=Code(url="www.example.com/ephys_compression"),
164+
)
165+
]
166+
)
167+
example_settings = JobSettings(output_directory="example", processing=new_processing)
168+
job = GatherProcessingJob(settings=example_settings)
169+
170+
with self.assertRaises(Exception) as context:
171+
job.run_job()
172+
173+
self.assertIn("Merge failed", str(context.exception))
174+
mock_log_error.assert_called_once()
175+
self.assertIn("Failed to merge existing processing.json", str(mock_log_error.call_args))
176+
mock_json_dump.assert_not_called()
177+
61178

62179
if __name__ == "__main__":
63180
unittest.main()

0 commit comments

Comments
 (0)