Skip to content

Commit 50457ce

Browse files
committed
enh: use orjson for robuster handling of IQMs (and NaNs)
Replaces the standard `json` library with `orjson`, which is way faster and more reliable in terms of encoding numpy objects (the main issue we typically hit within *MRIQC*). Incidentally, this PR fixes an import of `simplejson`, which was not listed as a dependency. Related-to: #1302. Closes: #546. Closes: #1089. Closes: #1133.
1 parent 73cd660 commit 50457ce

File tree

3 files changed

+33
-19
lines changed

3 files changed

+33
-19
lines changed

mriqc/interfaces/bids.py

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
import re
2424
from pathlib import Path
2525

26-
import simplejson as json
26+
import orjson as json
2727
from nipype.interfaces.base import (
2828
BaseInterfaceInputSpec,
2929
DynamicTraitedSpec,
@@ -187,16 +187,15 @@ def _run_interface(self, runtime):
187187
self._out_dict['provenance'] = {}
188188
self._out_dict['provenance'].update(prov_dict)
189189

190-
with open(out_file, 'w') as f:
191-
f.write(
192-
json.dumps(
193-
self._out_dict,
194-
sort_keys=True,
195-
indent=2,
196-
ensure_ascii=False,
197-
)
198-
)
199-
190+
Path(out_file).write_bytes(json.dumps(
191+
self._out_dict,
192+
option=(
193+
json.OPT_SORT_KEYS
194+
| json.OPT_INDENT_2
195+
| json.OPT_APPEND_NEWLINE
196+
| json.OPT_SERIALIZE_NUMPY
197+
),
198+
))
200199
return runtime
201200

202201

mriqc/interfaces/webapi.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,9 @@
2020
#
2121
# https://www.nipreps.org/community/licensing/
2222
#
23-
import json
23+
from pathlib import Path
2424

25+
import orjson
2526
from nipype.interfaces.base import (
2627
BaseInterfaceInputSpec,
2728
Bunch,
@@ -127,6 +128,7 @@ class UploadIQMsInputSpec(BaseInterfaceInputSpec):
127128

128129
class UploadIQMsOutputSpec(TraitedSpec):
129130
api_id = traits.Either(None, traits.Str, desc='Id for report returned by the web api')
131+
payload_file = File(desc='Submitted payload (only for debugging)')
130132

131133

132134
class UploadIQMs(SimpleInterface):
@@ -153,6 +155,19 @@ def _run_interface(self, runtime):
153155
modality=self.inputs.modality,
154156
)
155157

158+
159+
payload_str = orjson.dumps(
160+
payload,
161+
option=(
162+
orjson.OPT_SORT_KEYS
163+
| orjson.OPT_INDENT_2
164+
| orjson.OPT_APPEND_NEWLINE
165+
| orjson.OPT_SERIALIZE_NUMPY
166+
),
167+
)
168+
Path('payload.json').write_bytes(payload_str)
169+
self._results['payload_file'] = str(Path('payload.json').absolute())
170+
156171
try:
157172
self._results['api_id'] = response.json()['_id']
158173
except (AttributeError, KeyError, ValueError):
@@ -161,7 +176,7 @@ def _run_interface(self, runtime):
161176
'QC metrics upload failed to create an ID for the record '
162177
f'uploaded. Response from server follows: {response.text}'
163178
'\n\nPayload:\n'
164-
f'{json.dumps(payload, indent=2)}'
179+
f'{payload_str}'
165180
)
166181
config.loggers.interface.warning(errmsg)
167182

@@ -177,7 +192,7 @@ def _run_interface(self, runtime):
177192
'',
178193
'',
179194
'Payload:',
180-
json.dumps(payload, indent=2),
195+
payload_str,
181196
]
182197
)
183198
config.loggers.interface.warning(errmsg)
@@ -209,8 +224,6 @@ def upload_qc_metrics(
209224
210225
"""
211226
from copy import deepcopy
212-
from json import dumps, loads
213-
from pathlib import Path
214227

215228
import requests
216229

@@ -219,7 +232,7 @@ def upload_qc_metrics(
219232
errmsg = 'Unknown API endpoint' if not endpoint else 'Authentication failed.'
220233
return Bunch(status_code=1, text=errmsg)
221234

222-
in_data = loads(Path(in_iqms).read_text())
235+
in_data = orjson.loads(Path(in_iqms).read_bytes())
223236

224237
# Extract metadata and provenance
225238
meta = in_data.pop('bids_meta')
@@ -267,17 +280,18 @@ def upload_qc_metrics(
267280

268281
start_message = messages.QC_UPLOAD_START.format(url=endpoint)
269282
config.loggers.interface.info(start_message)
283+
270284
try:
271285
# if the modality is bold, call "bold" endpoint
272286
response = requests.post(
273287
f'{endpoint}/{modality}',
274288
headers=headers,
275-
data=dumps(data),
289+
data=orjson.dumps(data, option=orjson.OPT_SERIALIZE_NUMPY),
276290
timeout=15,
277291
)
278292
except requests.ConnectionError as err:
279293
errmsg = f'QC metrics failed to upload due to connection error shown below:\n{err}'
280-
return Bunch(status_code=1, text=errmsg)
294+
return Bunch(status_code=1, text=errmsg), data
281295

282296
return response, data
283297

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ dependencies = [
3030
"nitransforms ~= 24.0",
3131
"niworkflows ~=1.10.1",
3232
"numpy ~=1.20",
33+
"orjson",
3334
"pandas",
3435
"pybids >= 0.15.6",
3536
"PyYAML",

0 commit comments

Comments
 (0)