Skip to content

Commit ef9d0df

Browse files
fix: Split Funding and Investigators (#430)
* fix: Split Funding and Investigators * linter fixes * interrogate * fix: updates get_investigators tests * tests: adds extra unit test * integration test --------- Co-authored-by: Jon Young <104453205+jtyoung84@users.noreply.github.com>
1 parent 631592e commit ef9d0df

8 files changed

Lines changed: 154 additions & 83 deletions

File tree

scripts/integration_test.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
subject_id="804670",
3737
project_name="Learning mFISH-V1omFISH",
3838
modalities=[Modality.POPHYS, Modality.BEHAVIOR_VIDEOS, Modality.BEHAVIOR],
39+
metadata_service_url="http://aind-metadata-service-dev", # DEV metadata service for testing
3940
)
4041

4142
job = GatherMetadataJob(settings=settings)
@@ -72,7 +73,7 @@
7273
print("✗ Failed to fetch subject")
7374

7475
print("\nFetching procedures from metadata service...")
75-
procedures = job.get_procedures()
76+
procedures = job.get_procedures(subject_id="804670")
7677
if procedures:
7778
print(f"✓ Procedures fetched for subject: {procedures.get('subject_id')}")
7879
if procedures.get("subject_procedures"):
@@ -83,14 +84,16 @@
8384
print("✗ Failed to fetch procedures")
8485

8586
print("\nFetching funding from metadata service...")
86-
funding_source, investigators = job.get_funding()
87+
funding_source = job.get_funding()
8788
if funding_source:
8889
print(f"✓ Funding fetched: {len(funding_source)} funding source(s)")
8990
for fund in funding_source:
9091
print(f" Funder: {fund.get('funder', {}).get('name', 'Unknown')}")
9192
else:
9293
print("✗ No funding information found")
9394

95+
print("\nFetching investigators from metadata service...")
96+
investigators = job.get_investigators()
9497
if investigators:
9598
print(f"✓ Investigators fetched: {len(investigators)} investigator(s)")
9699
for inv in investigators:

src/aind_metadata_mapper/base.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
"""Base classes for metadata mappers"""
22

33
from pathlib import Path
4+
45
from pydantic import BaseModel
56

67

src/aind_metadata_mapper/fip/mapper.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -303,9 +303,7 @@ def transform(
303303

304304
# Get timing from all data streams (handle multiple epochs)
305305
# Find earliest start_time and latest end_time across all epochs
306-
start_times = [
307-
ensure_timezone(normalize_utc_timezone(ds["start_time"])) for ds in data_streams
308-
]
306+
start_times = [ensure_timezone(normalize_utc_timezone(ds["start_time"])) for ds in data_streams]
309307
end_times = [ensure_timezone(normalize_utc_timezone(ds["end_time"])) for ds in data_streams]
310308

311309
earliest_start = min(start_times)

src/aind_metadata_mapper/gather_metadata.py

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -133,46 +133,48 @@ def _get_prefixed_files_from_directory(self, directory: str, file_name_prefix: s
133133
contents.append(json.load(f))
134134
return contents
135135

136-
def get_funding(self) -> tuple[list, list]:
137-
"""Get funding and investigators metadata from the V2 endpoint
136+
def get_funding(self) -> list:
137+
"""Get funding metadata from the V2 endpoint
138138
139139
Returns
140140
-------
141-
tuple[list, list]
142-
A tuple of (funding_source, investigators)
141+
list
142+
A list of funding sources
143143
"""
144144
if not self.settings.project_name:
145-
return [], []
145+
return []
146146

147147
funding_url = f"{self.settings.metadata_service_url}" f"/api/v2/funding/{self.settings.project_name}"
148148
funding_info = metadata_service_helper(funding_url)
149+
return funding_info if funding_info else []
149150

150-
if not funding_info:
151-
return [], []
152-
153-
investigators = []
154-
parsed_funding_info = []
155-
156-
for f in funding_info:
157-
project_investigators = f.get("investigators", [])
158-
investigators.extend(project_investigators)
151+
def get_investigators(self) -> list:
152+
"""Get investigators metadata from the V2 endpoint
159153
160-
funding_info_without_investigators = {k: v for k, v in f.items() if k != "investigators"}
161-
parsed_funding_info.append(funding_info_without_investigators)
154+
Returns
155+
-------
156+
list
157+
A list of investigators
158+
"""
159+
if not self.settings.project_name:
160+
return []
162161

162+
investigators_url = (
163+
f"{self.settings.metadata_service_url}" f"/api/v2/investigators/{self.settings.project_name}"
164+
)
165+
investigators_info = metadata_service_helper(investigators_url)
166+
if investigators_info is None:
167+
return []
163168
# Deduplicate investigators by name and sort
164169
seen_names = set()
165170
unique_investigators = []
166-
for investigator in investigators:
171+
for investigator in investigators_info:
167172
name = investigator.get("name", "")
168173
if name and name not in seen_names:
169174
seen_names.add(name)
170175
unique_investigators.append(investigator)
171-
172176
unique_investigators.sort(key=lambda x: x.get("name", ""))
173-
investigators_list = unique_investigators
174-
175-
return parsed_funding_info, investigators_list
177+
return unique_investigators
176178

177179
def build_data_description(self, acquisition_start_time: str, subject_id: str) -> dict:
178180
"""Build data description metadata"""
@@ -184,14 +186,13 @@ def build_data_description(self, acquisition_start_time: str, subject_id: str) -
184186
logging.debug(f"Using existing {file_name}.")
185187
return self._get_file_from_user_defined_directory(file_name=file_name)
186188

187-
acquisition_start_time = normalize_utc_timezone(
188-
acquisition_start_time
189-
) # remove when we're past Python 3.11
189+
acquisition_start_time = normalize_utc_timezone(acquisition_start_time) # remove when we're past Python 3.11
190190
creation_time = datetime.fromisoformat(acquisition_start_time)
191191
logging.info(f"Using acquisition start time: {creation_time}")
192192

193193
# Get funding information
194-
funding_source, investigators = self.get_funding()
194+
funding_source = self.get_funding()
195+
investigators = self.get_investigators()
195196

196197
# Get modalities
197198
modalities = self.settings.modalities
@@ -513,9 +514,7 @@ def _validate_acquisition_start_time(self, acquisition_start_time: str) -> str:
513514
ValueError
514515
If acquisition_start_time doesn't match settings and raise_if_invalid is True
515516
"""
516-
acquisition_start_time = normalize_utc_timezone(
517-
acquisition_start_time
518-
) # remove when we're past Python 3.11
517+
acquisition_start_time = normalize_utc_timezone(acquisition_start_time) # remove when we're past Python 3.11
519518
local_acq_start_time = datetime.fromisoformat(acquisition_start_time)
520519

521520
if self.settings.acquisition_start_time and local_acq_start_time != self.settings.acquisition_start_time:
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
[{"object_type":"Funding information","funder":{"name":"Allen Institute","abbreviation":"AI","registry":"Research Organization Registry (ROR)","registry_identifier":"03cpe7c52"},"grant_number":null,"fundee":[{"object_type":"Person","name":"Marina Garrett","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Peter Groblewski","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Shawn Olsen","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null}],"investigators":[{"object_type":"Person","name":"Marina Garrett","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Peter Groblewski","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Shawn Olsen","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null}]},{"object_type":"Funding information","funder":{"name":"National Institute of Mental Health","abbreviation":"NIMH","registry":"Research Organization Registry (ROR)","registry_identifier":"04xeg9z08"},"grant_number":"U01MH130907","fundee":[{"object_type":"Person","name":"Marina Garrett","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null}],"investigators":[{"object_type":"Person","name":"Marina Garrett","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Peter Groblewski","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Shawn Olsen","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null}]}]
1+
[{"object_type":"Funding","funder":{"name":"Allen Institute","abbreviation":"AI","registry":"Research Organization Registry (ROR)","registry_identifier":"03cpe7c52"},"grant_number":null,"fundee":[{"object_type":"Person","name":"Marina Garrett","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Peter Groblewski","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Shawn Olsen","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null}]}]
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
[{"object_type":"Person","name":"Marina Garrett","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Peter Groblewski","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null},{"object_type":"Person","name":"Shawn Olsen","registry":"Open Researcher and Contributor ID (ORCID)","registry_identifier":null}]

tests/test_gather_metadata.py

Lines changed: 104 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from pathlib import Path
1313
from unittest.mock import MagicMock, mock_open, patch
1414

15-
from aind_data_schema.components.identifiers import Person
1615
from aind_data_schema.core.acquisition import Acquisition
1716
from aind_data_schema_models.modalities import Modality
1817
from aind_data_schema_models.organizations import Organization
@@ -290,11 +289,11 @@ def test_get_funding_no_project_name(self, mock_makedirs, mock_get):
290289
acquisition_start_time=datetime(2023, 1, 1, 12, 0, 0),
291290
)
292291
)
293-
funding, investigators = job_no_project.get_funding()
292+
funding = job_no_project.get_funding()
294293

295294
self.assertEqual(funding, [])
296-
self.assertEqual(investigators, [])
297295
mock_get.assert_not_called()
296+
mock_makedirs.assert_called()
298297

299298
@patch("requests.get")
300299
def test_get_funding_success_single_result(self, mock_get):
@@ -305,20 +304,14 @@ def test_get_funding_success_single_result(self, mock_get):
305304
{
306305
"funder": "Test Funder",
307306
"award_number": "12345",
308-
"investigators": [
309-
Person(name="John Doe").model_dump(),
310-
Person(name="Jane Smith").model_dump(),
311-
],
312307
}
313308
]
314309
mock_get.return_value = mock_response
315310

316-
funding, investigators = self.job.get_funding()
311+
funding = self.job.get_funding()
317312

318313
expected_funding = [{"funder": "Test Funder", "award_number": "12345"}]
319314
self.assertEqual(funding, expected_funding)
320-
self.assertEqual(len(investigators), 2)
321-
self.assertIsInstance(investigators[0], dict)
322315

323316
@patch("requests.get")
324317
def test_get_funding_success_multiple_results(self, mock_get):
@@ -329,58 +322,109 @@ def test_get_funding_success_multiple_results(self, mock_get):
329322
{
330323
"funder": "Funder 1",
331324
"award_number": "111",
332-
"investigators": [{"name": "Alice"}],
333325
},
334326
{
335327
"funder": "Funder 2",
336328
"award_number": "222",
337-
"investigators": [{"name": "Bob"}, {"name": "Charlie"}],
338329
},
339330
]
340331
mock_get.return_value = mock_response
341332

342-
funding, investigators = self.job.get_funding()
333+
funding = self.job.get_funding()
343334

344335
self.assertEqual(len(funding), 2)
345-
self.assertEqual(len(investigators), 3)
346336

347337
@patch("requests.get")
348-
def test_get_funding_empty_investigators(self, mock_get):
349-
"""Test get_funding with empty investigators"""
338+
@patch("os.makedirs")
339+
def test_get_investigators_no_project_name(self, mock_makedirs, mock_get):
340+
"""Test get_investigators when no project name is provided"""
341+
job_no_project = GatherMetadataJob(
342+
JobSettings(
343+
metadata_dir="/test",
344+
output_dir="/test/output",
345+
subject_id="test_subject",
346+
project_name="",
347+
modalities=[Modality.ECEPHYS],
348+
acquisition_start_time=datetime(2023, 1, 1, 12, 0, 0),
349+
)
350+
)
351+
investigators = job_no_project.get_investigators()
352+
353+
self.assertEqual(investigators, [])
354+
mock_get.assert_not_called()
355+
mock_makedirs.assert_called()
356+
357+
@patch("aind_metadata_mapper.gather_metadata.metadata_service_helper")
358+
@patch("os.makedirs")
359+
def test_get_investigators_helper_issue(self, mock_makedirs, mock_metadata_helper):
360+
"""Test get_investigators when helper function returns None"""
361+
job_no_project = GatherMetadataJob(
362+
JobSettings(
363+
metadata_dir="/test",
364+
output_dir="/test/output",
365+
subject_id="test_subject",
366+
project_name="Some Project",
367+
modalities=[Modality.ECEPHYS],
368+
acquisition_start_time=datetime(2023, 1, 1, 12, 0, 0),
369+
)
370+
)
371+
mock_metadata_helper.return_value = None
372+
investigators = job_no_project.get_investigators()
373+
374+
self.assertEqual(investigators, [])
375+
mock_makedirs.assert_called()
376+
377+
@patch("requests.get")
378+
def test_get_investigators_success_single_result(self, mock_get):
379+
"""Test get_investigators with successful single result"""
350380
mock_response = MagicMock()
351381
mock_response.status_code = 200
352382
mock_response.json.return_value = [
353383
{
354-
"funder": "Test Funder",
355-
"award_number": "12345",
356-
"investigators": [],
384+
"object_type": "Person",
385+
"name": "Jane Doe",
386+
"registry": "Open Researcher and Contributor ID (ORCID)",
387+
"registry_identifier": None,
357388
}
358389
]
359390
mock_get.return_value = mock_response
360391

361-
funding, investigators = self.job.get_funding()
362-
363-
self.assertEqual(len(investigators), 0)
392+
investigators = self.job.get_investigators()
364393

365-
@patch("aind_metadata_mapper.utils.metadata_service_helper")
366-
def test_get_funding_http_error(self, mock_helper):
367-
"""Test get_funding with HTTP error"""
368-
mock_helper.return_value = None
369-
370-
funding, investigators = self.job.get_funding()
371-
372-
self.assertEqual(funding, [])
373-
self.assertEqual(investigators, [])
394+
expected_investigators = [
395+
{
396+
"object_type": "Person",
397+
"name": "Jane Doe",
398+
"registry": "Open Researcher and Contributor ID (ORCID)",
399+
"registry_identifier": None,
400+
}
401+
]
402+
self.assertEqual(investigators, expected_investigators)
374403

375-
@patch("aind_metadata_mapper.utils.metadata_service_helper")
376-
def test_get_funding_request_exception(self, mock_helper):
377-
"""Test get_funding with request exception"""
378-
mock_helper.return_value = None
404+
@patch("requests.get")
405+
def test_get_investigators_success_multiple_results(self, mock_get):
406+
"""Test get_investigators with successful multiple results"""
407+
mock_response = MagicMock()
408+
mock_response.status_code = 200
409+
mock_response.json.return_value = [
410+
{
411+
"object_type": "Person",
412+
"name": "Jane Doe",
413+
"registry": "Open Researcher and Contributor ID (ORCID)",
414+
"registry_identifier": None,
415+
},
416+
{
417+
"object_type": "Person",
418+
"name": "John Smith",
419+
"registry": "Open Researcher and Contributor ID (ORCID)",
420+
"registry_identifier": None,
421+
},
422+
]
423+
mock_get.return_value = mock_response
379424

380-
funding, investigators = self.job.get_funding()
425+
investigators = self.job.get_investigators()
381426

382-
self.assertEqual(funding, [])
383-
self.assertEqual(investigators, [])
427+
self.assertEqual(len(investigators), 2)
384428

385429
# Tests for _write_json_file method
386430
@patch("builtins.open", new_callable=mock_open)
@@ -416,14 +460,31 @@ def test_build_data_description_existing_file(self, mock_get_file, mock_file_exi
416460

417461
@patch.object(GatherMetadataJob, "_does_file_exist_in_user_defined_dir")
418462
@patch.object(GatherMetadataJob, "get_funding")
463+
@patch.object(GatherMetadataJob, "get_investigators")
419464
@patch("aind_metadata_mapper.gather_metadata.datetime")
420-
def test_build_data_description_new_file(self, mock_datetime, mock_get_funding, mock_file_exists):
465+
def test_build_data_description_new_file(
466+
self, mock_datetime, mock_get_investigators, mock_get_funding, mock_file_exists
467+
):
421468
"""Test build_data_description when creating new file"""
422469
mock_file_exists.return_value = False
423-
mock_get_funding.return_value = (
424-
[{"funder": Organization.NIMH, "grant_number": "12345"}],
425-
[{"name": "Test Investigator"}],
426-
)
470+
mock_get_funding.return_value = [
471+
{
472+
"object_type": "Funding",
473+
"funder": Organization.NIMH,
474+
"grant_number": "12345",
475+
"fundee": [
476+
{
477+
"object_type": "Person",
478+
"name": "Jane Doe",
479+
"registry": "Addgene (ADDGENE)",
480+
"registry_identifier": None,
481+
}
482+
],
483+
}
484+
]
485+
mock_get_investigators.return_value = [
486+
{"object_type": "Person", "name": "Jane Doe", "registry": "Addgene (ADDGENE)", "registry_identifier": None}
487+
]
427488
mock_datetime.now.return_value = datetime(2023, 1, 1, 12, 0, 0)
428489

429490
result = self.job.build_data_description(acquisition_start_time="2023-01-01T12:00:00", subject_id="123456")

0 commit comments

Comments
 (0)