Skip to content

Commit

Permalink
Add support for URL prefix of RESTful services
Browse files Browse the repository at this point in the history
Some DICOMweb services may use a separate URL path prefix for QIDO-RS,
WADO-RS and STOW-RS. They can now be passed to the constructor of the
DICOMwebClient class, such that the same instance can be used for
searching, retrieving and storing data.
  • Loading branch information
hackermd committed Jul 23, 2018
1 parent 283b0e9 commit 16d7d7c
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 48 deletions.
27 changes: 15 additions & 12 deletions docs/usage.rst
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,38 @@ The client can be used with any DICOMweb server, such as `dcm4che <http://www.dc
Examples
--------

For the examples below, we will use the publicly accessible `RESTful DICOM services provided by DICOMweb Cloud <https://dicomcloud.azurewebsites.net>`_ (note that URLs and UIDs may be subject to change).
For the examples below, we will use the publicly accessible `RESTful services provided by DICOMweb Cloud <https://dicomcloud.azurewebsites.net>`_ (note that *DICOMweb Cloud* uses different URL prefixes for the RESTful services QIDO-RS, WADO-RS and STOW-RS).


Active Programming Interface (API)
++++++++++++++++++++++++++++++++++

Search for instances:
Instantiate the client:

.. code-block:: python
from dicomweb_client.api import DICOMwebClient
qidors = DICOMwebClient(url="https://dicomcloud.azurewebsites.net/qidors")
instances = qidors.search_for_instances()
client = DICOMwebClient(
url="https://dicomcloud.azurewebsites.net",
qido_url_prefix="qidors", wado_url_prefix="wadors",
stow_url_prefix="stowrs"
)
Search for instances:

.. code-block:: python
instances = client.search_for_instances()
print(instances)
Retrieve metadata for all instances of a given study:

.. code-block:: python
from dicomweb_client.api import DICOMwebClient
wadors = DICOMwebClient(url="https://dicomcloud.azurewebsites.net/wadors")
study_instance_uid = '1.2.826.0.1.3680043.8.1055.1.20111103111148288.98361414.79379639'
study_metadata = wadors.retrieve_study_metadata(study_instance_uid)
study_metadata = client.retrieve_study_metadata(study_instance_uid)
print(study_metadata)
Expand All @@ -46,13 +52,10 @@ Retrieve a single frame of a given instances as JPEG compressed image and show i
from PIL import Image
from io import BytesIO
from dicomweb_client.api import DICOMwebClient
wadors = DICOMwebClient(url="https://dicomcloud.azurewebsites.net/wadors")
study_instance_uid = '1.2.826.0.1.3680043.8.1055.1.20111103111148288.98361414.79379639'
series_instance_uid = '1.2.826.0.1.3680043.8.1055.1.20111103111208937.49685336.24517034'
sop_instance_uid = '1.2.826.0.1.3680043.8.1055.1.20111103111208937.40440871.13152534'
frames = wadors.retrieve_instance_frames(
frames = client.retrieve_instance_frames(
study_instance_uid, series_instance_uid, sop_instance_uid,
frame_numbers=[1], image_format='jpeg'
)
Expand Down
124 changes: 88 additions & 36 deletions src/dicomweb_client/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,14 @@ def _create_dataelement(tag, vr, value):
if value is None:
# TODO: check if mandatory
logger.warn('missing value for data element "{}"'.format(tag))
return pydicom.dataelem.DataElement(tag=tag, value=elem_value, VR=vr)
try:
return pydicom.dataelem.DataElement(tag=tag, value=elem_value, VR=vr)
except Exception as e:
raise ValueError(
'Data element "{}" could not be loaded from JSON: {}'.format(
tag, elem_value
)
)


def load_json_dataset(dataset):
Expand Down Expand Up @@ -169,30 +176,47 @@ class DICOMwebClient(object):
IP address or DNS name of the machine that hosts the server
port: int
number of the port to which the server listens
prefix: str
URL prefix
url_prefix: str
URL path prefix for DICOMweb services (part of `base_url`)
qido_url_prefix: Union[str, NoneType]
URL path prefix for QIDO-RS (not part of `base_url`)
wado_url_prefix: Union[str, NoneType]
URL path prefix for WADO-RS (not part of `base_url`)
stow_url_prefix: Union[str, NoneType]
URL path prefix for STOW-RS (not part of `base_url`)
'''

def __init__(self, url, username=None, password=None, ca_bundle=None):
def __init__(self, url, username=None, password=None, ca_bundle=None,
qido_url_prefix=None, wado_url_prefix=None,
stow_url_prefix=None):
'''
Parameters
----------
url: str
base unique resource locator consisting of protocol, hostname
(IP address or DNS name) of the machine that hosts the server and
optionally port number and path prefix
username: str
username for authentication with the server
password: str
password for authentication with the server
username: str, optional
username for authentication with services
password: str, optional
password for authentication with services
ca_bundle: str, optional
path to CA bundle file in Privacy Enhanced Mail (PEM) format
qido_url_prefix: str, optional
URL path prefix for QIDO RESTful services
wado_url_prefix: str, optional
URL path prefix for WADO RESTful services
stow_url_prefix: str, optional
URL path prefix for STOW RESTful services
'''
logger.debug('initialize HTTP session')
self._session = requests.Session()
self.base_url = url
self.qido_url_prefix = qido_url_prefix
self.wado_url_prefix = wado_url_prefix
self.stow_url_prefix = stow_url_prefix
if self.base_url.startswith('https'):
if ca_bundle is not None:
ca_bundle = os.path.expanduser(os.path.expandvars(ca_bundle))
Expand Down Expand Up @@ -229,9 +253,9 @@ def __init__(self, url, username=None, password=None, ca_bundle=None):
raise ValueError(
'URL scheme "{}" is not supported.'.format(self.protocol)
)
self.prefix = match.group('prefix')
if self.prefix is None:
self.prefix = ''
self.url_prefix = match.group('prefix')
if self.url_prefix is None:
self.url_prefix = ''
self._session.headers.update({'Host': self.host})
if username is not None:
if not password:
Expand Down Expand Up @@ -276,19 +300,37 @@ def _parse_query_parameters(self, fuzzymatching, limit, offset,
# Sort query parameters to facilitate unit testing
return OrderedDict(sorted(params.items()))

def _get_studies_url(self, study_instance_uid=None):
def _get_service_url(self, service):
service_url = self.base_url
if service == 'qido':
if self.qido_url_prefix is not None:
service_url += '/{}'.format(self.qido_url_prefix)
elif service == 'wado':
if self.wado_url_prefix is not None:
service_url += '/{}'.format(self.wado_url_prefix)
elif service == 'stow':
if self.stow_url_prefix is not None:
service_url += '/{}'.format(self.stow_url_prefix)
else:
raise ValueError(
'Unsupported DICOMweb service "{}".'.format(service)
)
return service_url

def _get_studies_url(self, service, study_instance_uid=None):
if study_instance_uid is not None:
url = '{service}/studies/{study_instance_uid}'
url = '{service_url}/studies/{study_instance_uid}'
else:
url = '{service}/studies'
url = '{service_url}/studies'
service_url = self._get_service_url(service)
return url.format(
service=self.base_url, study_instance_uid=study_instance_uid
service_url=service_url, study_instance_uid=study_instance_uid
)

def _get_series_url(self, study_instance_uid=None,
def _get_series_url(self, service, study_instance_uid=None,
series_instance_uid=None):
if study_instance_uid is not None:
url = self._get_studies_url(study_instance_uid)
url = self._get_studies_url(service, study_instance_uid)
if series_instance_uid is not None:
url += '/series/{series_instance_uid}'
else:
Expand All @@ -298,27 +340,31 @@ def _get_series_url(self, study_instance_uid=None,
logger.warn(
'series UID is ignored because study UID is undefined'
)
url = '{service}/series'
url = '{service_url}/series'
service_url = self._get_service_url(service)
return url.format(
service=self.base_url, series_instance_uid=series_instance_uid
service_url=service_url, series_instance_uid=series_instance_uid
)

def _get_instances_url(self, study_instance_uid=None,
def _get_instances_url(self, service, study_instance_uid=None,
series_instance_uid=None, sop_instance_uid=None):
if study_instance_uid is not None and series_instance_uid is not None:
url = self._get_series_url(study_instance_uid, series_instance_uid)
url = self._get_series_url(
service, study_instance_uid, series_instance_uid
)
url += '/instances'
if sop_instance_uid is not None:
url += '/{sop_instance_uid}'
else:
if sop_instance_uid is not None:
logger.warn(
'instance UID is ignored because study and/or instance '
'UID are undefined'
'SOP Instance UID is ignored because Study/Series '
'Instance UID are undefined'
)
url = '{service}/instances'
url = '{service_url}/instances'
service_url = self._get_service_url(service)
return url.format(
service=self.base_url, sop_instance_uid=sop_instance_uid
service_url=service_url, sop_instance_uid=sop_instance_uid
)

@staticmethod
Expand Down Expand Up @@ -620,7 +666,7 @@ def search_for_studies(self, fuzzymatching=None, limit=None, offset=None,
(see `returned attributes <http://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_6.7.html#table_6.7.1-2>`_)
''' # noqa
url = self._get_studies_url()
url = self._get_studies_url('qido')
params = self._parse_query_parameters(
fuzzymatching, limit, offset, fields, **search_filters
)
Expand Down Expand Up @@ -678,7 +724,7 @@ def retrieve_study(self, study_instance_uid):
raise ValueError(
'Study Instance UID is required for retrieval of study.'
)
url = self._get_studies_url(study_instance_uid)
url = self._get_studies_url('wado', study_instance_uid)
return self._http_get_multipart_application_dicom(url)

def retrieve_study_metadata(self, study_instance_uid):
Expand All @@ -700,7 +746,7 @@ def retrieve_study_metadata(self, study_instance_uid):
'Study Instance UID is required for retrieval of '
'study metadata.'
)
url = self._get_studies_url(study_instance_uid)
url = self._get_studies_url('wado', study_instance_uid)
url += '/metadata'
return self._http_get_application_json(url)

Expand Down Expand Up @@ -758,7 +804,7 @@ def search_for_series(self, study_instance_uid=None, fuzzymatching=None,
''' # noqa
if study_instance_uid is not None:
self._check_uid_format(study_instance_uid)
url = self._get_series_url(study_instance_uid)
url = self._get_series_url('qido', study_instance_uid)
params = self._parse_query_parameters(
fuzzymatching, limit, offset, fields, **search_filters
)
Expand Down Expand Up @@ -795,7 +841,9 @@ def retrieve_series(self, study_instance_uid, series_instance_uid):
'Series Instance UID is required for retrieval of series.'
)
self._check_uid_format(series_instance_uid)
url = self._get_series_url(study_instance_uid, series_instance_uid)
url = self._get_series_url(
'wado', study_instance_uid, series_instance_uid
)
return self._http_get_multipart_application_dicom(url)

def retrieve_series_metadata(self, study_instance_uid, series_instance_uid):
Expand Down Expand Up @@ -826,7 +874,9 @@ def retrieve_series_metadata(self, study_instance_uid, series_instance_uid):
'series metadata.'
)
self._check_uid_format(series_instance_uid)
url = self._get_series_url(study_instance_uid, series_instance_uid)
url = self._get_series_url(
'wado', study_instance_uid, series_instance_uid
)
url += '/metadata'
return self._http_get_application_json(url)

Expand Down Expand Up @@ -864,7 +914,9 @@ def search_for_instances(self, study_instance_uid=None,
''' # noqa
if study_instance_uid is not None:
self._check_uid_format(study_instance_uid)
url = self._get_instances_url(study_instance_uid, series_instance_uid)
url = self._get_instances_url(
'qido', study_instance_uid, series_instance_uid
)
params = self._parse_query_parameters(
fuzzymatching, limit, offset, fields, **search_filters
)
Expand Down Expand Up @@ -911,7 +963,7 @@ def retrieve_instance(self, study_instance_uid, series_instance_uid,
)
self._check_uid_format(sop_instance_uid)
url = self._get_instances_url(
study_instance_uid, series_instance_uid, sop_instance_uid
'wado', study_instance_uid, series_instance_uid, sop_instance_uid
)
return self._http_get_multipart_application_dicom(url)[0]

Expand All @@ -926,7 +978,7 @@ def store_instances(self, datasets, study_instance_uid=None):
unique study identifier
'''
url = self._get_studies_url(study_instance_uid)
url = self._get_studies_url('stow', study_instance_uid)
encoded_datasets = list()
# TODO: can we do this more memory efficient? Concatenations?
for ds in datasets:
Expand Down Expand Up @@ -970,7 +1022,7 @@ def retrieve_instance_metadata(self, study_instance_uid,
'instance metadata.'
)
url = self._get_instances_url(
study_instance_uid, series_instance_uid, sop_instance_uid
'wado', study_instance_uid, series_instance_uid, sop_instance_uid
)
url += '/metadata'
return self._http_get_application_json(url)
Expand Down Expand Up @@ -1018,7 +1070,7 @@ def retrieve_instance_frames(self, study_instance_uid, series_instance_uid,
'SOP Instance UID is required for retrieval of frames.'
)
url = self._get_instances_url(
study_instance_uid, series_instance_uid, sop_instance_uid
'wado', study_instance_uid, series_instance_uid, sop_instance_uid
)
frame_list = ','.join([str(n) for n in frame_numbers])
url += '/frames/{frame_list}'.format(frame_list=frame_list)
Expand Down
36 changes: 36 additions & 0 deletions src/dicomweb_client/tests/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,19 @@ def test_search_for_studies(httpserver, client, cache_dir):
assert request.accept_mimetypes[0][0] == 'application/dicom+json'


def test_search_for_studies_qido_prefix(httpserver, client, cache_dir):
client.qido_url_prefix = 'qidors'
cache_filename = os.path.join(cache_dir, 'search_for_studies.json')
with open(cache_filename, 'r') as f:
content = f.read()
parsed_content = json.loads(content)
headers = {'content-type': 'application/dicom+json'}
httpserver.serve_content(content=content, code=200, headers=headers)
studies = client.search_for_studies()
request = httpserver.requests[0]
assert request.path == '/qidors/studies'


def test_search_for_studies_limit_offset(httpserver, client, cache_dir):
cache_filename = os.path.join(cache_dir, 'search_for_studies.json')
with open(cache_filename, 'r') as f:
Expand Down Expand Up @@ -151,6 +164,29 @@ def test_retrieve_instance_metadata(httpserver, client, cache_dir):
assert request.accept_mimetypes[0][0] == 'application/dicom+json'


def test_retrieve_instance_metadata_wado_prefix(httpserver, client, cache_dir):
client.wado_url_prefix = 'wadors'
cache_filename = os.path.join(cache_dir, 'retrieve_instance_metadata.json')
with open(cache_filename, 'r') as f:
content = f.read()
parsed_content = json.loads(content)
headers = {'content-type': 'application/dicom+json'}
httpserver.serve_content(content=content, code=200, headers=headers)
study_instance_uid = '1.2.3'
series_instance_uid = '1.2.4'
sop_instance_uid = '1.2.5'
result = client.retrieve_instance_metadata(
study_instance_uid, series_instance_uid, sop_instance_uid
)
request = httpserver.requests[0]
expected_path = (
'/wadors/studies/{study_instance_uid}'
'/series/{series_instance_uid}'
'/instances/{sop_instance_uid}/metadata'.format(**locals())
)
assert request.path == expected_path


def test_retrieve_instance(httpserver, client, cache_dir):
cache_filename = os.path.join(cache_dir, 'file.dcm')
with open(cache_filename, 'rb') as f:
Expand Down

0 comments on commit 16d7d7c

Please sign in to comment.