Skip to content

Commit 35c1b0c

Browse files
authored
Merge pull request #148 from effigies/fix/xml-results
fix: Error on missing S3 files, do not write error data to disk
2 parents cb15b5d + 43d21a9 commit 35c1b0c

File tree

9 files changed

+95
-198
lines changed

9 files changed

+95
-198
lines changed

docs/environment.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ dependencies:
195195
- zlib=1.2.11=h166bdaf_1014
196196
- zstd=1.5.2=ha95c52a_0
197197
- pip:
198+
- acres
198199
- bids-validator==1.9.3
199200
- docopt==0.6.2
200201
- formulaic==0.3.4

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ classifiers = [
2121
license = {file = "LICENSE"}
2222
requires-python = ">=3.9"
2323
dependencies = [
24+
"acres >= 0.5.0",
2425
"pybids >= 0.15.2",
25-
"importlib_resources >= 5.7; python_version < '3.11'",
2626
"requests",
2727
"tqdm",
2828
]

templateflow/_loader.py

Lines changed: 0 additions & 193 deletions
This file was deleted.

templateflow/api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ def get(template, raise_empty=False, **kwargs):
159159
if raise_empty and not out_file:
160160
raise Exception('No results found')
161161

162+
# Truncate possible S3 error files from previous attempts
163+
_truncate_s3_errors(out_file)
164+
162165
# Try DataLad first
163166
dl_missing = [p for p in out_file if not p.is_file()]
164167
if TF_USE_DATALAD and dl_missing:
@@ -320,6 +323,8 @@ def _s3_get(filepath):
320323
print(f'Downloading {url}', file=stderr)
321324
# Streaming, so we can iterate over the response.
322325
r = requests.get(url, stream=True, timeout=TF_GET_TIMEOUT)
326+
if r.status_code != 200:
327+
raise RuntimeError(f'Failed to download {url} with status code {r.status_code}')
323328

324329
# Total size in bytes.
325330
total_size = int(r.headers.get('content-length', 0))
@@ -393,3 +398,20 @@ def _normalize_ext(value):
393398
if isinstance(value, str):
394399
return f'{"" if value.startswith(".") else "."}{value}'
395400
return [_normalize_ext(v) for v in value]
401+
402+
403+
def _truncate_s3_errors(filepaths):
404+
"""
405+
Truncate XML error bodies saved by previous versions of TemplateFlow.
406+
407+
Parameters
408+
----------
409+
filepaths : list of Path
410+
List of file paths to check and truncate if necessary.
411+
"""
412+
for filepath in filepaths:
413+
if filepath.is_file(follow_symlinks=False) and 0 < filepath.stat().st_size < 1024:
414+
with open(filepath, 'rb') as f:
415+
content = f.read(100)
416+
if content.startswith(b'<?xml') and b'<Error><Code>' in content:
417+
filepath.write_bytes(b'') # Truncate file to zero bytes

templateflow/cli.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
from click.decorators import FC, Option, _param_memo
3232

3333
from templateflow import __package__, api
34-
from templateflow._loader import Loader as _Loader
34+
from acres import Loader as _Loader
3535
from templateflow.conf import TF_AUTOUPDATE, TF_HOME, TF_USE_DATALAD
3636

3737
load_data = _Loader(__package__)
@@ -58,7 +58,7 @@ def _nulls(s):
5858
def entity_opts():
5959
"""Attaches all entities as options to the command."""
6060

61-
entities = json.loads(Path(load_data('conf/config.json')).read_text())['entities']
61+
entities = json.loads(load_data('conf/config.json').read_text())['entities']
6262

6363
args = [
6464
(f'--{e["name"]}', *ENTITY_SHORTHANDS.get(e['name'], ()))

templateflow/conf/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77
from pathlib import Path
88
from warnings import warn
99

10-
from .._loader import Loader
10+
from acres import Loader
1111

12-
load_data = Loader(__package__)
12+
load_data = Loader(__spec__.name)
1313

1414

1515
def _env_to_bool(envvar: str, default: bool) -> bool:
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from acres import Loader
2+
3+
load_data = Loader(__spec__.name)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<Error><Code>InvalidArgument</Code><Message>Invalid version id specified</Message><ArgumentName>versionId</ArgumentName><ArgumentValue>test</ArgumentValue><RequestId>BKT12MP069SFQGH3</RequestId><HostId>DIljS3MUsLCEa27wSyqAxsZZE3MhqEWYf3lRbH2Rl19VV0pGe/61Hh3MzSBeS45VltnZDzliHaTMxjnGPvKOOk+SY/it3Ond</HostId></Error>

templateflow/tests/test_s3.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@
2525
from importlib import reload
2626
from pathlib import Path
2727

28+
import pytest
2829
import requests
2930

31+
from templateflow import api as tf
3032
from templateflow import conf as tfc
3133

34+
from .data import load_data
35+
3236

3337
def test_get_skel_file(tmp_path, monkeypatch):
3438
"""Exercise the skeleton file generation."""
@@ -87,3 +91,61 @@ def test_update_s3(tmp_path, monkeypatch):
8791
for p in (newhome / 'tpl-MNI152NLin6Sym').glob('*.nii.gz'):
8892
p.unlink()
8993
assert tfc._s3.update(newhome, local=False, overwrite=False)
94+
95+
96+
def mock_get(*args, **kwargs):
97+
class MockResponse:
98+
status_code = 400
99+
100+
return MockResponse()
101+
102+
103+
def test_s3_400_error(monkeypatch):
104+
"""Simulate a 400 error when fetching the skeleton file."""
105+
106+
reload(tfc)
107+
reload(tf)
108+
109+
monkeypatch.setattr(requests, 'get', mock_get)
110+
with pytest.raises(RuntimeError, match=r'Failed to download .* code 400'):
111+
tf._s3_get(
112+
Path(tfc.TF_LAYOUT.root)
113+
/ 'tpl-MNI152NLin2009cAsym/tpl-MNI152NLin2009cAsym_res-02_T1w.nii.gz'
114+
)
115+
116+
117+
def test_bad_skeleton(tmp_path, monkeypatch):
118+
newhome = (tmp_path / 's3-update').resolve()
119+
monkeypatch.setattr(tfc, 'TF_USE_DATALAD', False)
120+
monkeypatch.setattr(tfc, 'TF_HOME', newhome)
121+
monkeypatch.setattr(tfc, 'TF_LAYOUT', None)
122+
123+
tfc._init_cache()
124+
tfc.init_layout()
125+
126+
assert tfc.TF_LAYOUT is not None
127+
assert tfc.TF_LAYOUT.root == str(newhome)
128+
129+
# Instead of reloading
130+
monkeypatch.setattr(tf, 'TF_LAYOUT', tfc.TF_LAYOUT)
131+
132+
paths = tf.ls('MNI152NLin2009cAsym', resolution='02', suffix='T1w', desc=None)
133+
assert paths
134+
path = Path(paths[0])
135+
assert path.read_bytes() == b''
136+
137+
error_file = load_data.readable('error_response.xml')
138+
path.write_bytes(error_file.read_bytes())
139+
140+
# Test directly before testing through API paths
141+
tf._truncate_s3_errors(paths)
142+
assert path.read_bytes() == b''
143+
144+
path.write_bytes(error_file.read_bytes())
145+
146+
monkeypatch.setattr(requests, 'get', mock_get)
147+
with pytest.raises(RuntimeError):
148+
tf.get('MNI152NLin2009cAsym', resolution='02', suffix='T1w', desc=None)
149+
150+
# Running get clears bad files before attempting to download
151+
assert path.read_bytes() == b''

0 commit comments

Comments
 (0)