Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #520 from oesteban/enh/508
[ENH] Integration testing for MRIQCWebAPI
- Loading branch information
Showing
12 changed files
with
347 additions
and
125 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,34 @@ | ||
#!/bin/bash | ||
# | ||
# Balance nipype testing workflows across CircleCI build nodes | ||
# | ||
|
||
# Setting # $ help set | ||
set -e # Exit immediately if a command exits with a non-zero status. | ||
set -u # Treat unset variables as an error when substituting. | ||
set -x # Print command traces before executing command. | ||
|
||
# Exit if build_only tag is found | ||
if [ "$(grep -qiP 'build[ _]?only' <<< "$GIT_COMMIT_MSG"; echo $? )" == "0" ]; then | ||
exit 0 | ||
fi | ||
|
||
# Exit if docs_only tag is found | ||
if [ "$(grep -qiP 'docs[ _]?only' <<< "$GIT_COMMIT_MSG"; echo $? )" == "0" ]; then | ||
echo "Building [docs_only], nothing to do." | ||
exit 0 | ||
fi | ||
|
||
MODALITY=T1w | ||
NRECORDS=4 | ||
if [ "$CIRCLE_NODE_INDEX" == "1" ]; then | ||
MODALITY=bold | ||
NRECORDS=9 | ||
fi | ||
|
||
echo "Checking records in MRIQC Web API" | ||
docker run -i --entrypoint="/usr/local/miniconda/bin/python" \ | ||
${DOCKER_IMAGE}:${DOCKER_TAG} \ | ||
/root/src/mriqc/mriqc/bin/mriqcwebapi_test.py \ | ||
${MODALITY} ${NRECORDS} \ | ||
--webapi-addr $( hostname -I | awk '{print $1}' ) --webapi-port ${MRIQC_API_PORT} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
# @Author: oesteban | ||
# @Date: 2015-11-19 16:44:27 | ||
from __future__ import print_function, division, absolute_import, unicode_literals | ||
|
||
def get_parser(): | ||
"""Build parser object""" | ||
from argparse import ArgumentParser | ||
from argparse import RawTextHelpFormatter | ||
|
||
parser = ArgumentParser(description='MRIQCWebAPI: Check entries', | ||
formatter_class=RawTextHelpFormatter) | ||
parser.add_argument('modality', action='store', choices=['T1w', 'bold'], | ||
help='number of expected items in the database') | ||
parser.add_argument('expected', action='store', type=int, | ||
help='number of expected items in the database') | ||
parser.add_argument( | ||
'--webapi-addr', action='store', default='34.201.213.252', type=str, | ||
help='IP address where the MRIQC WebAPI is listening') | ||
parser.add_argument( | ||
'--webapi-port', action='store', default=80, type=int, | ||
help='port where the MRIQC WebAPI is listening') | ||
return parser | ||
|
||
|
||
def main(): | ||
"""Entry point""" | ||
from requests import get | ||
from mriqc import MRIQC_LOG | ||
|
||
# Run parser | ||
opts = get_parser().parse_args() | ||
|
||
endpoint = 'http://{}:{}/{}'.format(opts.webapi_addr, | ||
opts.webapi_port, | ||
opts.modality) | ||
MRIQC_LOG.info('Sending GET: %s', endpoint) | ||
resp = get(endpoint).json() | ||
MRIQC_LOG.info('There are %d records in database', resp['_meta']['total']) | ||
assert opts.expected == resp['_meta']['total'] | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
#!/usr/bin/env python | ||
# -*- coding: utf-8 -*- | ||
# emacs: -*- mode: python; py-indent-offset: 4; indent-tabs-mode: nil -*- | ||
# vi: set ft=python sts=4 ts=4 sw=4 et: | ||
from __future__ import print_function, division, absolute_import, unicode_literals | ||
|
||
from nipype import logging | ||
from nipype.interfaces.base import (Bunch, traits, isdefined, TraitedSpec, | ||
BaseInterfaceInputSpec, File, Str) | ||
from niworkflows.interfaces.base import SimpleInterface | ||
|
||
|
||
IFLOGGER = logging.getLogger('interface') | ||
|
||
SECRET_KEY = """\ | ||
ZUsBaabr6PEbav5DKAHIODEnwpwC58oQTJF7KWvDBPUmBIVFFtw\ | ||
Od7lQBdz9r9ulJTR1BtxBDqDuY0owxK6LbLB1u1b64ZkIMd46\ | ||
""" | ||
|
||
# metadata whitelist | ||
META_WHITELIST = [ | ||
'AccelNumReferenceLines', | ||
'AccelerationFactorPE', | ||
'AcquisitionMatrix', | ||
'CogAtlasID', | ||
'CogPOID', | ||
'CoilCombinationMethod', | ||
'ContrastBolusIngredient', | ||
'ConversionSoftware', | ||
'ConversionSoftwareVersion', | ||
'DelayTime', | ||
'DeviceSerialNumber', | ||
'EchoTime', | ||
'EchoTrainLength', | ||
'EffectiveEchoSpacing', | ||
'FlipAngle', | ||
'GradientSetType', | ||
'HardcopyDeviceSoftwareVersion', | ||
'ImageType', | ||
'ImagingFrequency', | ||
'InPlanePhaseEncodingDirection', | ||
'InstitutionAddress', | ||
'InstitutionName', | ||
'Instructions', | ||
'InversionTime', | ||
'MRAcquisitionType', | ||
'MRTransmitCoilSequence', | ||
'MagneticFieldStrength', | ||
'Manufacturer', | ||
'ManufacturersModelName', | ||
'MatrixCoilMode', | ||
'MultibandAccelerationFactor', | ||
'NumberOfAverages', | ||
'NumberOfPhaseEncodingSteps', | ||
'NumberOfVolumesDiscardedByScanner', | ||
'NumberOfVolumesDiscardedByUser', | ||
'NumberShots', | ||
'ParallelAcquisitionTechnique', | ||
'ParallelReductionFactorInPlane', | ||
'PartialFourier', | ||
'PartialFourierDirection', | ||
'PatientPosition', | ||
'PercentPhaseFieldOfView', | ||
'PercentSampling', | ||
'PhaseEncodingDirection', | ||
'PixelBandwidth', | ||
'ProtocolName', | ||
'PulseSequenceDetails', | ||
'PulseSequenceType', | ||
'ReceiveCoilName', | ||
'RepetitionTime', | ||
'ScanOptions', | ||
'ScanningSequence', | ||
'SequenceName', | ||
'SequenceVariant', | ||
'SliceEncodingDirection', | ||
'SoftwareVersions', | ||
'TaskDescription', | ||
'TaskName', | ||
'TotalReadoutTime', | ||
'TotalScanTimeSec', | ||
'TransmitCoilName', | ||
'VariableFlipAngleFlag', | ||
'acq_id', | ||
'modality', | ||
'run_id', | ||
'subject_id', | ||
'task_id', | ||
] | ||
|
||
PROV_WHITELIST = [ | ||
'version', | ||
'md5sum', | ||
'software', | ||
'settings' | ||
] | ||
|
||
|
||
class UploadIQMsInputSpec(BaseInterfaceInputSpec): | ||
in_iqms = File(exists=True, mandatory=True, desc='the input IQMs-JSON file') | ||
address = Str(mandatory=True, desc='ip address listening') | ||
port = traits.Int(mandatory=True, desc='MRIQCWebAPI service port') | ||
email = Str(desc='set sender email') | ||
strict = traits.Bool(False, usedefault=True, | ||
desc='crash if upload was not succesfull') | ||
|
||
|
||
class UploadIQMs(SimpleInterface): | ||
""" | ||
Upload features to MRIQCWebAPI | ||
""" | ||
|
||
input_spec = UploadIQMsInputSpec | ||
output_spec = TraitedSpec | ||
|
||
def _run_interface(self, runtime): | ||
email = None | ||
if isdefined(self.inputs.email): | ||
email = self.inputs.email | ||
|
||
response = upload_qc_metrics( | ||
self.inputs.in_iqms, | ||
self.inputs.address, | ||
self.inputs.port, | ||
) | ||
|
||
if response.status_code == 201: | ||
IFLOGGER.info('QC metrics successfully uploaded.') | ||
return runtime | ||
|
||
errmsg = 'QC metrics failed to upload. Status %d: %s' % ( | ||
response.status_code, response.text) | ||
IFLOGGER.warn(errmsg) | ||
if self.inputs.strict: | ||
raise RuntimeError(response.text) | ||
|
||
return runtime | ||
|
||
|
||
def upload_qc_metrics(in_iqms, addr, port, email=None): | ||
""" | ||
Upload qc metrics to remote repository. | ||
:param str in_iqms: Path to the qc metric json file as a string | ||
:param str email: email address to be included with the metric submission | ||
:param bool no_sub: Flag from settings indicating whether or not metrics should be submitted. | ||
If False, metrics will be submitted. If True, metrics will not be submitted. | ||
:param str mriqc_webapi: the default mriqcWebAPI url | ||
:param bool upload_strict: the client should fail if it's strict mode | ||
:return: either the response object if a response was successfully sent | ||
or it returns the string "No Response" | ||
:rtype: object | ||
""" | ||
from json import load, dumps | ||
import requests | ||
from io import open | ||
from copy import deepcopy | ||
|
||
with open(in_iqms, 'r') as input_json: | ||
in_data = load(input_json) | ||
|
||
# Extract metadata and provenance | ||
meta = in_data.pop('bids_meta') | ||
prov = in_data.pop('provenance') | ||
|
||
# At this point, data should contain only IQMs | ||
data = deepcopy(in_data) | ||
|
||
# Check modality | ||
modality = meta.get('modality', 'None') | ||
if modality not in ('T1w', 'bold'): | ||
errmsg = ('Submitting to MRIQCWebAPI: image modality should be "bold" or "T1w", ' | ||
'(found "%s")' % modality) | ||
return Bunch(status_code=1, text=errmsg) | ||
|
||
# Filter metadata values that aren't in whitelist | ||
data['bids_meta'] = {k: meta[k] for k in META_WHITELIST if k in meta} | ||
# Filter provenance values that aren't in whitelist | ||
data['provenance'] = {k: prov[k] for k in PROV_WHITELIST if k in prov} | ||
|
||
if email: | ||
data['email'] = email | ||
|
||
headers = {'token': SECRET_KEY, "Content-Type": "application/json"} | ||
try: | ||
# if the modality is bold, call "bold" endpointt | ||
response = requests.post( | ||
'http://{}:{}/{}'.format(addr, port, modality), | ||
headers=headers, data=dumps(data)) | ||
except requests.ConnectionError as err: | ||
errmsg = 'QC metrics failed to upload due to connection error shown below:\n%s' % err | ||
return Bunch(status_code=1, text=errmsg) | ||
|
||
return response |
Oops, something went wrong.