Skip to content

Commit bdcba16

Browse files
authored
make it easier to download/work with neurostore studyset (#1078)
1 parent 4da7952 commit bdcba16

7 files changed

Lines changed: 20407 additions & 21 deletions

File tree

docs/api.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ For more information about functional characterization analysis, see :doc:`decod
146146
io.convert_neurosynth_to_dict
147147
io.convert_neurosynth_to_json
148148
io.convert_neurosynth_to_dataset
149+
io.fetch_neurostore_studyset
149150
io.convert_nimads_to_dataset
150151
io.convert_nimads_to_sleuth
151152
io.convert_dataset_to_nimads_dict

examples/01_datasets/05_plot_nimads.py

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -12,34 +12,15 @@
1212
"""
1313

1414
from pprint import pprint
15-
from requests import request
1615

16+
from nimare.io import fetch_neurostore_studyset
1717
from nimare.meta.cbma import ALE
18-
from nimare.nimads import Studyset
19-
2018

2119
###############################################################################
2220
# Download Data from NeuroStore
2321
# -----------------------------------------------------------------------------
2422

25-
26-
def download_file(url):
27-
"""Download a file from NeuroStore."""
28-
response = request("GET", url)
29-
return response.json()
30-
31-
32-
# Download a studyset and its annotation
33-
nimads_studyset = download_file("https://neurostore.org/api/studysets/Cv2LLUqG76W9?nested=true")
34-
nimads_annotation = download_file("https://neurostore.org/api/annotations/76PyNqoTNEsE")
35-
36-
37-
###############################################################################
38-
# Create and Explore Studyset
39-
# -----------------------------------------------------------------------------
40-
# Load the data into a NiMADS Studyset object and explore its contents
41-
42-
studyset = Studyset(nimads_studyset, annotations=nimads_annotation)
23+
studyset = fetch_neurostore_studyset("Cv2LLUqG76W9", annotation_id="76PyNqoTNEsE")
4324

4425
# Display basic information about the studyset
4526
print("\nStudyset Information:")

nimare/io.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,56 @@ def _extract_coordinate_row_metadata(metadata, n_points):
185185
return coordinate_rows, coordinate_keys
186186

187187

188+
def fetch_neurostore_studyset(studyset_id, annotation_id=None, target="mni152_2mm"):
189+
"""Download a Neurostore studyset and optional annotation as a NiMARE Studyset.
190+
191+
Parameters
192+
----------
193+
studyset_id : :obj:`str`
194+
Neurostore studyset ID to download. The studyset is requested as a nested
195+
payload.
196+
annotation_id : :obj:`str`, optional
197+
Neurostore annotation ID to download and attach to the studyset. The regular
198+
annotation payload is requested.
199+
target : :obj:`str` or None, optional
200+
Target template space for coordinates. Default is ``"mni152_2mm"``, matching
201+
:class:`~nimare.nimads.Studyset`.
202+
203+
Returns
204+
-------
205+
studyset : :obj:`nimare.nimads.Studyset`
206+
Downloaded studyset with the optional annotation attached.
207+
"""
208+
from neurostore_sdk import ApiClient, ApiException, StoreApi
209+
210+
from nimare.nimads import Studyset
211+
212+
store_api = StoreApi()
213+
api_client = ApiClient()
214+
215+
try:
216+
studyset_response = store_api.studysets_id_get(studyset_id, nested=True)
217+
except ApiException as exc:
218+
raise ValueError(f"Failed to download Neurostore studyset '{studyset_id}'.") from exc
219+
220+
studyset_payload = api_client.sanitize_for_serialization(studyset_response)
221+
222+
annotation_payload = None
223+
if annotation_id is not None:
224+
try:
225+
annotation_response = store_api.annotations_id_get(annotation_id)
226+
except ApiException as exc:
227+
raise ValueError(
228+
f"Failed to download Neurostore annotation '{annotation_id}'."
229+
) from exc
230+
231+
annotation_payload = api_client.sanitize_for_serialization(annotation_response)
232+
233+
if annotation_payload is None:
234+
return Studyset(studyset_payload, target=target)
235+
return Studyset(studyset_payload, annotations=annotation_payload, target=target)
236+
237+
188238
def convert_nimads_to_dataset(studyset, annotation=None):
189239
"""Convert nimads studyset to a dataset.
190240

nimare/tests/cassettes/test_io/test_fetch_neurostore_studyset.yaml

Lines changed: 9753 additions & 0 deletions
Large diffs are not rendered by default.

nimare/tests/cassettes/test_io/test_fetch_neurostore_studyset_with_annotation.yaml

Lines changed: 10520 additions & 0 deletions
Large diffs are not rendered by default.

nimare/tests/test_io.py

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,85 @@
1313
from nimare.tests.utils import get_test_data_path
1414
from nimare.utils import get_template
1515

16+
NEUROSTORE_STUDYSET_ID = "qm2PZBqNsaZK"
17+
NEUROSTORE_ANNOTATION_ID = "hbTQJVL2kAb8"
18+
19+
20+
@pytest.fixture(scope="module")
21+
def vcr_cassette_dir():
22+
"""Store test_io cassettes in a stable directory."""
23+
return os.path.join(os.path.dirname(__file__), "cassettes", "test_io")
24+
25+
26+
@pytest.fixture(scope="module")
27+
def vcr_config():
28+
"""Keep Neurostore cassettes stable and free of credentials."""
29+
return {
30+
"filter_headers": ["authorization"],
31+
"decode_compressed_response": True,
32+
}
33+
34+
35+
@pytest.mark.vcr
36+
def test_fetch_neurostore_studyset():
37+
"""Download a nested Neurostore studyset into a NiMARE Studyset."""
38+
studyset = io.fetch_neurostore_studyset(NEUROSTORE_STUDYSET_ID, target="ale_2mm")
39+
40+
assert isinstance(studyset, Studyset)
41+
assert studyset.id == NEUROSTORE_STUDYSET_ID
42+
assert studyset.space == "ale_2mm"
43+
assert len(studyset.studies) > 0
44+
45+
46+
@pytest.mark.vcr
47+
def test_fetch_neurostore_studyset_with_annotation():
48+
"""Download a nested Neurostore studyset and attach a regular annotation."""
49+
studyset = io.fetch_neurostore_studyset(
50+
NEUROSTORE_STUDYSET_ID,
51+
annotation_id=NEUROSTORE_ANNOTATION_ID,
52+
)
53+
54+
assert isinstance(studyset, Studyset)
55+
assert studyset.id == NEUROSTORE_STUDYSET_ID
56+
assert len(studyset.annotations) == 1
57+
assert studyset.annotations[0].id == NEUROSTORE_ANNOTATION_ID
58+
assert "included" in studyset.annotations_df.columns
59+
assert studyset.annotations_df["included"].any()
60+
61+
62+
def test_fetch_neurostore_studyset_wraps_api_errors(monkeypatch):
63+
"""Neurostore request failures should report the resource that failed."""
64+
import neurostore_sdk
65+
66+
class FailingStoreApi:
67+
def studysets_id_get(self, id, nested=True):
68+
raise neurostore_sdk.ApiException(status=404, reason="Not Found")
69+
70+
monkeypatch.setattr(neurostore_sdk, "StoreApi", FailingStoreApi)
71+
72+
with pytest.raises(ValueError, match="Failed to download Neurostore studyset 'bad-id'"):
73+
io.fetch_neurostore_studyset("bad-id")
74+
75+
76+
def test_fetch_neurostore_studyset_wraps_annotation_api_errors(monkeypatch):
77+
"""Neurostore annotation request failures should report the annotation ID."""
78+
import neurostore_sdk
79+
80+
class FailingAnnotationStoreApi:
81+
def studysets_id_get(self, id, nested=True):
82+
return {"id": "ok-id", "studies": []}
83+
84+
def annotations_id_get(self, id):
85+
raise neurostore_sdk.ApiException(status=404, reason="Not Found")
86+
87+
monkeypatch.setattr(neurostore_sdk, "StoreApi", FailingAnnotationStoreApi)
88+
89+
with pytest.raises(
90+
ValueError,
91+
match="Failed to download Neurostore annotation 'bad-annotation'",
92+
):
93+
io.fetch_neurostore_studyset("ok-id", annotation_id="bad-annotation")
94+
1695

1796
def test_convert_nimads_to_dataset(example_nimads_studyset, example_nimads_annotation):
1897
"""Conversion of nimads JSON to nimare dataset."""

setup.cfg

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ install_requires =
3232
matplotlib>=3.6.0 # this is for nilearn, which doesn't include it in its reqs
3333
nibabel>=5.2.0 # I/O of niftis
3434
nilearn>=0.12.0,<0.14
35+
neurostore-sdk>=1.1 # nimare.io Neurostore fetching
3536
numba>=0.57.0 # used by sparse
3637
numpy>=1.22.4 # numba needs NumPy 1.22 or greater
3738
pandas>=2.1.4
@@ -79,6 +80,7 @@ tests =
7980
flake8-isort
8081
pytest
8182
pytest-cov
83+
pytest-vcr
8284
minimum =
8385
matplotlib==3.6.0; python_version < "3.11"
8486
nibabel==5.2.0; python_version < "3.11"

0 commit comments

Comments
 (0)