Skip to content

Commit

Permalink
Merge pull request #520 from oesteban/enh/508
Browse files Browse the repository at this point in the history
[ENH] Integration testing for MRIQCWebAPI
  • Loading branch information
chrisgorgo committed Jun 1, 2017
2 parents 4ec19cb + 6157fc4 commit 91368d1
Show file tree
Hide file tree
Showing 12 changed files with 347 additions and 125 deletions.
3 changes: 2 additions & 1 deletion .circleci/participant.sh
Expand Up @@ -32,7 +32,8 @@ DOCKER_RUN="docker run -i -v $HOME/data:/data:ro \
-v $SCRATCH:/scratch -w /scratch \
${DOCKER_IMAGE}:${DOCKER_TAG} \
/data/${TEST_DATA_NAME} out/ participant \
--verbose-reports --profile"
--verbose-reports --profile \
--webapi-addr $( hostname -I | awk '{print $1}' ) --webapi-port ${MRIQC_API_PORT} --upload-strict"

case $CIRCLE_NODE_INDEX in
0)
Expand Down
34 changes: 34 additions & 0 deletions .circleci/webapi.sh
@@ -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}
20 changes: 18 additions & 2 deletions circle.yml
Expand Up @@ -8,13 +8,17 @@ machine:
DOCKER_TAG: "latest"
TEST_DATA_NAME: "circle-tests"
TEST_DATA_URL: "https://files.osf.io/v1/resources/fvuh8/providers/osfstorage/590ce4a96c613b025147c568"
SECRET_KEY: CI
MRIQC_API_PORT: 80
MRIQC_API_TAG: 0.3.0
services:
- docker

dependencies:
cache_directories:
- "~/docker"
- "~/data"
- "~/mriqcwebapi"

pre:
# Download test data
Expand All @@ -23,6 +27,14 @@ dependencies:
- mkdir -p $SCRATCH && sudo setfacl -d -m group:ubuntu:rwx $SCRATCH && sudo setfacl -m group:ubuntu:rwx $SCRATCH
- if [[ ! -d ~/data/${TEST_DATA_NAME} ]]; then wget --retry-connrefused --waitretry=5 --read-timeout=20 --timeout=15 -t 0 -q -O ${TEST_DATA_NAME}.tar.gz "${TEST_DATA_URL}" && tar xzf ${TEST_DATA_NAME}.tar.gz -C ~/data/; fi
- docker load --input $HOME/docker/cache.tar || true
# Prepare MRIQCWebAPI
- pip install docker-compose
- if [[ ! -d $HOME/mriqcwebapi ]]; then cd; git clone https://github.com/poldracklab/mriqcwebapi.git; fi;
- cd $HOME/mriqcwebapi && git fetch --tags && git checkout ${MRIQC_API_TAG}
- docker-compose -f $HOME/mriqcwebapi/dockereve-master/docker-compose.yml pull
- docker-compose -f $HOME/mriqcwebapi/dockereve-master/docker-compose.yml build
- nohup bash -c "docker-compose -f $HOME/mriqcwebapi/dockereve-master/docker-compose.yml --verbose up -d" && sleep 10
- docker run -it --entrypoint=/usr/bin/curl ${DOCKER_IMAGE}:${DOCKER_TAG} --retry 10 --retry-delay 15 -vkf http://$( hostname -I | awk '{print $1}' )
override:
- echo "${CIRCLE_TAG:-$CIRCLE_SHA1}" > mriqc/VERSION
- ? |
Expand All @@ -31,7 +43,7 @@ dependencies:
done && [ "$e" -eq "0" ]
:
timeout: 3200
- docker save -o $HOME/docker/cache.tar ubuntu:xenial-20161213 ${DOCKER_IMAGE}:${DOCKER_TAG} :
- docker save -o $HOME/docker/cache.tar ubuntu:xenial-20161213 ${DOCKER_IMAGE}:${DOCKER_TAG} python:3.4-onbuild tutum/nginx:latest mongo:latest :
timeout: 3200
test:
override:
Expand All @@ -53,6 +65,10 @@ test:
parallel: true
environment:
GIT_COMMIT_MSG: $( git log --format=oneline -n 1 $CIRCLE_SHA1 )
- bash .circleci/webapi.sh :
parallel: true
environment:
GIT_COMMIT_MSG: $( git log --format=oneline -n 1 $CIRCLE_SHA1 )

general:
artifacts:
Expand All @@ -77,7 +93,7 @@ deployment:
echo "This is not a release candidate, pushing ${DOCKER_IMAGE}:${DOCKER_TAG}"
docker push ${DOCKER_IMAGE}:${DOCKER_TAG}
fi
fi
fi
:
timeout: 21600
- |
Expand Down
15 changes: 13 additions & 2 deletions mriqc/bin/mriqc_run.py
Expand Up @@ -85,6 +85,15 @@ def get_parser():
g_outputs.add_argument('--email', action='store', default='', type=str,
help='Email address to include with quality metric submission.')

g_outputs.add_argument(
'--webapi-addr', action='store', default='34.201.213.252', type=str,
help='IP address where the MRIQC WebAPI is listening')
g_outputs.add_argument(
'--webapi-port', action='store', default=80, type=int,
help='port where the MRIQC WebAPI is listening')

g_outputs.add_argument('--upload-strict', action='store_true', default=False,
help='upload will fail if if upload is strict')
# General performance
g_perfm = parser.add_argument_group('Options to handle performance')
g_perfm.add_argument('--n_procs', '--nprocs', '--n_cpus', '--nprocs',
Expand All @@ -98,7 +107,6 @@ def get_parser():
help="Cast the input data to float32 if it's represented in higher precision "
"(saves space and improves perfomance)")


# Workflow settings
g_conf = parser.add_argument_group('Workflow configuration')
g_conf.add_argument('--ica', action='store_true', default=False,
Expand Down Expand Up @@ -170,9 +178,12 @@ def main():
'verbose_reports': opts.verbose_reports or opts.testing,
'float32': opts.float32,
'ica': opts.ica,
'no_sub': opts.no_sub or opts.testing,
'no_sub': opts.no_sub,
'email': opts.email,
'fd_thres': opts.fd_thres,
'webapi_addr' : opts.webapi_addr,
'webapi_port' : opts.webapi_port,
'upload_strict' : opts.upload_strict,
}

if opts.hmc_afni:
Expand Down
45 changes: 45 additions & 0 deletions mriqc/bin/mriqcwebapi_test.py
@@ -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()
1 change: 1 addition & 0 deletions mriqc/interfaces/__init__.py
Expand Up @@ -14,3 +14,4 @@
from mriqc.interfaces.bids import ReadSidecarJSON, IQMFileSink
from mriqc.interfaces.viz import PlotMosaic, PlotContours, PlotSpikes
from mriqc.interfaces.common import ConformImage, EnsureSize
from mriqc.interfaces.webapi import UploadIQMs
198 changes: 198 additions & 0 deletions mriqc/interfaces/webapi.py
@@ -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,
email
)

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

0 comments on commit 91368d1

Please sign in to comment.