Skip to content

dicomtrolley.wado_rs

Models WADO-RS: Web Access to dicom Objects by Restful Services

[Wado RS description] (https://www.dicomstandard.org/using/dicomweb/retrieve-wado-rs-and-wado-uri/)

See Also

[DICOM part18 section 10.4] (https://dicom.nema.org/medical/dicom/current/output/chtml/part18/sect_10.4.html)

WadoRS

Bases: Downloader

A connection to a WADO-RS endpoints for downloading full datasets

Source code in dicomtrolley/wado_rs.py
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
class WadoRS(Downloader):
    """A connection to a WADO-RS endpoints for downloading full datasets"""

    def __init__(
        self, session, url, http_chunk_size=5242880, request_per_series=True
    ):
        """
        Parameters
        ----------
        session: requests.session
            A logged-in session over which WADO calls can be made
        url: str
            WADO-RS endpoint, including protocol and port. Like
            https://server:8080/wado
        http_chunk_size: int, optional
            Number of bytes to read each time when streaming chunked responses.
            Defaults to 5MB (5242880 bytes)
        request_per_series: bool, optional
            If true, split requests per series when downloading. If false,
            request all instances at once. Splitting reduces load on server.
            defaults to True.
        """

        self.session = session
        self.url = url
        self.http_chunk_size = http_chunk_size
        self.request_per_series = request_per_series

    def datasets(self, objects: Sequence[DICOMDownloadable]):
        """Retrieve each instance

        Returns
        -------
        Iterator[Dataset, None, None]

        Raises
        ------
        NonSeriesParameterError
            If request_per_series is True and objects contains study references
            without series information.

        DICOMTrolleyError
            If getting does not work for some reason
        """
        logger.debug("Getting datasets")
        if isinstance(objects, DICOMDownloadable):
            objects = [objects]  # handle passing single object instead of list

        if self.request_per_series:
            references: Sequence[DICOMDownloadable] = to_series_level_refs(
                # references: Sequence[DICOMDownloadable] = to_instance_refs(
                objects
            )
            logger.debug(
                f"Splitting {len(objects)} objects into series. After split,"
                f" getting {len(references)} downloadables"
            )
        else:
            references = objects

        return chain.from_iterable(
            self.download_iterator(obj) for obj in references
        )

    def download_iterator(self, downloadable: DICOMDownloadable):
        """Perform a wado RS request and iterate over the returned datasets.

        Returns
        -------
        Iterator[Dataset, None, None]
            All datasets included in the response
        """
        uri = self.wado_rs_instance_uri(downloadable.reference())
        logger.debug(f"Calling {uri}")
        response = self.session.get(
            url=uri,
            stream=True,
        )

        return self.parse(response)

    def parse(self, response) -> Iterator[Dataset]:
        """Extract datasets out of http response from a WADO-RS server

        Parameters
        ----------
        response:
            A requests response objects, requests with stream=True

        Raises
        ------
        DICOMTrolleyError
            If response is not as expected or if parsing fails.

        Returns
        -------
        Iterator[Dataset, None, None]
            All datasets included in this response
        """

        logger.debug("Parsing WADO-RS response")

        self.check_for_response_errors(response)

        part_stream = HTTPMultiPartStream(
            response, stream_chunk_size=self.http_chunk_size
        )
        for part in part_stream:
            raw = DicomBytesIO(part.content)
            try:
                yield dcmread(raw)
            except InvalidDicomError as e:
                raise DICOMTrolleyError(
                    f"Error parsing response as dicom: {e}."
                    f" Response content (first 300 elements) was"
                    f" {str(response.content[:300])}"
                ) from e
            except OSError as e:  # pydicom might validly raise this on bad DICOM
                raise DICOMTrolleyError(
                    f"Error parsing response as DICOM: {e}"
                ) from e

    @staticmethod
    def check_for_response_errors(response):
        """Raise exceptions if this response is not a valid WADO-RS response.

        Parameters
        ----------
        response: response
            response as returned from a wado-rs call

        Raises
        ------
        DICOMTrolleyError
            If response is not as expected
        """
        if response.status_code != 200:
            raise DICOMTrolleyError(
                f"Calling {response.url} failed ({response.status_code} - "
                f"{response.reason})\n"
                f"response content was {str(response.content[:300])}"
            )

        # check multipart
        if "Content-type" not in response.headers:
            raise DICOMTrolleyError(
                f"Expected multipart response, but got no content type for this"
                f" response. Start of response: {str(response.content[:300])}"
            )

    def wado_rs_instance_uri(self, reference: DICOMObjectReference):
        """WADO-RS URI to request all instances contained in referenced object"""
        uri = self.url.rstrip(
            "/"
        )  # self.url might or might not have trailing /
        if isinstance(reference, StudyReference):
            return f"{uri}/studies/{reference.study_uid}"
        elif isinstance(reference, SeriesReference):
            return (
                f"{uri}/studies/{reference.study_uid}/series"
                f"/{reference.series_uid}"
            )
        elif isinstance(reference, InstanceReference):
            return (
                f"{uri}/studies/{reference.study_uid}/series"
                f"/{reference.series_uid}/instances/{reference.instance_uid}"
            )
        else:
            raise ValueError(
                f"StudyReference or InstanceReference expected. "
                f"Found {type(reference)}"
            )

__init__(session, url, http_chunk_size=5242880, request_per_series=True)

Parameters

session: requests.session A logged-in session over which WADO calls can be made url: str WADO-RS endpoint, including protocol and port. Like https://server:8080/wado http_chunk_size: int, optional Number of bytes to read each time when streaming chunked responses. Defaults to 5MB (5242880 bytes) request_per_series: bool, optional If true, split requests per series when downloading. If false, request all instances at once. Splitting reduces load on server. defaults to True.

Source code in dicomtrolley/wado_rs.py
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def __init__(
    self, session, url, http_chunk_size=5242880, request_per_series=True
):
    """
    Parameters
    ----------
    session: requests.session
        A logged-in session over which WADO calls can be made
    url: str
        WADO-RS endpoint, including protocol and port. Like
        https://server:8080/wado
    http_chunk_size: int, optional
        Number of bytes to read each time when streaming chunked responses.
        Defaults to 5MB (5242880 bytes)
    request_per_series: bool, optional
        If true, split requests per series when downloading. If false,
        request all instances at once. Splitting reduces load on server.
        defaults to True.
    """

    self.session = session
    self.url = url
    self.http_chunk_size = http_chunk_size
    self.request_per_series = request_per_series

check_for_response_errors(response) staticmethod

Raise exceptions if this response is not a valid WADO-RS response.

Parameters

response: response response as returned from a wado-rs call

Raises

DICOMTrolleyError If response is not as expected

Source code in dicomtrolley/wado_rs.py
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
@staticmethod
def check_for_response_errors(response):
    """Raise exceptions if this response is not a valid WADO-RS response.

    Parameters
    ----------
    response: response
        response as returned from a wado-rs call

    Raises
    ------
    DICOMTrolleyError
        If response is not as expected
    """
    if response.status_code != 200:
        raise DICOMTrolleyError(
            f"Calling {response.url} failed ({response.status_code} - "
            f"{response.reason})\n"
            f"response content was {str(response.content[:300])}"
        )

    # check multipart
    if "Content-type" not in response.headers:
        raise DICOMTrolleyError(
            f"Expected multipart response, but got no content type for this"
            f" response. Start of response: {str(response.content[:300])}"
        )

datasets(objects)

Retrieve each instance

Returns

Iterator[Dataset, None, None]

Raises

NonSeriesParameterError If request_per_series is True and objects contains study references without series information.

DICOMTrolleyError If getting does not work for some reason

Source code in dicomtrolley/wado_rs.py
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
def datasets(self, objects: Sequence[DICOMDownloadable]):
    """Retrieve each instance

    Returns
    -------
    Iterator[Dataset, None, None]

    Raises
    ------
    NonSeriesParameterError
        If request_per_series is True and objects contains study references
        without series information.

    DICOMTrolleyError
        If getting does not work for some reason
    """
    logger.debug("Getting datasets")
    if isinstance(objects, DICOMDownloadable):
        objects = [objects]  # handle passing single object instead of list

    if self.request_per_series:
        references: Sequence[DICOMDownloadable] = to_series_level_refs(
            # references: Sequence[DICOMDownloadable] = to_instance_refs(
            objects
        )
        logger.debug(
            f"Splitting {len(objects)} objects into series. After split,"
            f" getting {len(references)} downloadables"
        )
    else:
        references = objects

    return chain.from_iterable(
        self.download_iterator(obj) for obj in references
    )

download_iterator(downloadable)

Perform a wado RS request and iterate over the returned datasets.

Returns

Iterator[Dataset, None, None] All datasets included in the response

Source code in dicomtrolley/wado_rs.py
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
def download_iterator(self, downloadable: DICOMDownloadable):
    """Perform a wado RS request and iterate over the returned datasets.

    Returns
    -------
    Iterator[Dataset, None, None]
        All datasets included in the response
    """
    uri = self.wado_rs_instance_uri(downloadable.reference())
    logger.debug(f"Calling {uri}")
    response = self.session.get(
        url=uri,
        stream=True,
    )

    return self.parse(response)

parse(response)

Extract datasets out of http response from a WADO-RS server

Parameters

response: A requests response objects, requests with stream=True

Raises

DICOMTrolleyError If response is not as expected or if parsing fails.

Returns

Iterator[Dataset, None, None] All datasets included in this response

Source code in dicomtrolley/wado_rs.py
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
def parse(self, response) -> Iterator[Dataset]:
    """Extract datasets out of http response from a WADO-RS server

    Parameters
    ----------
    response:
        A requests response objects, requests with stream=True

    Raises
    ------
    DICOMTrolleyError
        If response is not as expected or if parsing fails.

    Returns
    -------
    Iterator[Dataset, None, None]
        All datasets included in this response
    """

    logger.debug("Parsing WADO-RS response")

    self.check_for_response_errors(response)

    part_stream = HTTPMultiPartStream(
        response, stream_chunk_size=self.http_chunk_size
    )
    for part in part_stream:
        raw = DicomBytesIO(part.content)
        try:
            yield dcmread(raw)
        except InvalidDicomError as e:
            raise DICOMTrolleyError(
                f"Error parsing response as dicom: {e}."
                f" Response content (first 300 elements) was"
                f" {str(response.content[:300])}"
            ) from e
        except OSError as e:  # pydicom might validly raise this on bad DICOM
            raise DICOMTrolleyError(
                f"Error parsing response as DICOM: {e}"
            ) from e

wado_rs_instance_uri(reference)

WADO-RS URI to request all instances contained in referenced object

Source code in dicomtrolley/wado_rs.py
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def wado_rs_instance_uri(self, reference: DICOMObjectReference):
    """WADO-RS URI to request all instances contained in referenced object"""
    uri = self.url.rstrip(
        "/"
    )  # self.url might or might not have trailing /
    if isinstance(reference, StudyReference):
        return f"{uri}/studies/{reference.study_uid}"
    elif isinstance(reference, SeriesReference):
        return (
            f"{uri}/studies/{reference.study_uid}/series"
            f"/{reference.series_uid}"
        )
    elif isinstance(reference, InstanceReference):
        return (
            f"{uri}/studies/{reference.study_uid}/series"
            f"/{reference.series_uid}/instances/{reference.instance_uid}"
        )
    else:
        raise ValueError(
            f"StudyReference or InstanceReference expected. "
            f"Found {type(reference)}"
        )

WadoRSMetaData

Bases: Downloader

A connection to WADO-RS to download only metadata, no PixelData

Source code in dicomtrolley/wado_rs.py
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
class WadoRSMetaData(Downloader):
    """A connection to WADO-RS to download only metadata, no PixelData"""

    def __init__(self, session, url):
        """
        Parameters
        ----------
        session: requests.session
            A logged-in session over which WADO calls can be made
        url: str
            WADO-RS endpoint, including protocol and port. Like
            https://server:8080/wado
        """

        self.session = session
        self.url = url

    def datasets(self, objects: Sequence[DICOMDownloadable]):
        """Retrieve each instance

        Returns
        -------
        Iterator[Dataset, None, None]

        Raises
        ------
        DICOMTrolleyError
            If getting does not work for some reason
        """
        logger.debug("Getting datasets")
        if isinstance(objects, DICOMDownloadable):
            objects = [objects]  # handle passing single object instead of list

        return chain.from_iterable(
            self.download_iterator(obj) for obj in objects
        )

    def wado_rs_instance_uri(self, reference: DICOMObjectReference):
        """WADO-RS URI to request all instances contained in referenced object"""
        uri = self.url.rstrip(
            "/"
        )  # self.url might or might not have trailing /
        if isinstance(reference, StudyReference):
            # Note: study-level /metadata is not in the DICOM standard, but some
            # VNA systems still implement it
            return f"{uri}/studies/{reference.study_uid}/metadata"
        elif isinstance(reference, SeriesReference):
            return (
                f"{uri}/studies/{reference.study_uid}/series"
                f"/{reference.series_uid}/metadata"
            )
        elif isinstance(reference, InstanceReference):
            return (
                f"{uri}/studies/{reference.study_uid}/series"
                f"/{reference.series_uid}/instances/{reference.instance_uid}/metadata"
            )
        else:
            raise ValueError(
                f"StudyReference or InstanceReference expected. "
                f"Found {type(reference)}"
            )

    @staticmethod
    def check_for_response_errors(response):
        """Raise exceptions if this response is not a valid WADO-RS response.

        Parameters
        ----------
        response: Response
            requests.Response as returned from a wado-rs call

        Raises
        ------
        NoQueryResults
            If http 204 (No Content) is returned by server
            see https://dicom.nema.org/medical/dicom/current/output/chtml/
            part18/sect_8.3.4.4.html
        DICOMTrolleyError
            If response is otherwise not as expected
        """
        if response.status_code == 204:
            raise NoQueryResultsError("Server returned http 204 (No Content)")
        elif response.status_code != 200:
            raise DICOMTrolleyError(
                f"Calling {response.url} failed ({response.status_code} - "
                f"{response.reason})\n"
                f"response content was {str(response.content[:300])}"
            )

    def download_iterator(self, downloadable: DICOMDownloadable):
        """Perform a wado RS request and iterate over the returned datasets.

        Returns
        -------
        Iterator[Dataset, None, None]
            All datasets included in the response
        """
        uri = self.wado_rs_instance_uri(downloadable.reference())
        logger.debug(f"Calling {uri}")
        response = self.session.get(
            url=uri,
            stream=True,
        )
        response_parsed = json.loads(response.text)
        logger.debug(
            f"Received {len(response_parsed)} metadata results. Parsing."
        )

        return iter(
            Dataset.from_json(x, bulk_data_uri_handler=self.bulk_data_reader)
            for x in response_parsed
        )

    @classmethod
    def bulk_data_reader(cls, tag, vr, bulk_data_uri):
        logger.debug(
            f"Metadata-only download. Not downloading bulk data "
            f"at {bulk_data_uri}"
        )

__init__(session, url)

Parameters

session: requests.session A logged-in session over which WADO calls can be made url: str WADO-RS endpoint, including protocol and port. Like https://server:8080/wado

Source code in dicomtrolley/wado_rs.py
213
214
215
216
217
218
219
220
221
222
223
224
225
def __init__(self, session, url):
    """
    Parameters
    ----------
    session: requests.session
        A logged-in session over which WADO calls can be made
    url: str
        WADO-RS endpoint, including protocol and port. Like
        https://server:8080/wado
    """

    self.session = session
    self.url = url

check_for_response_errors(response) staticmethod

Raise exceptions if this response is not a valid WADO-RS response.

Parameters

response: Response requests.Response as returned from a wado-rs call

Raises

NoQueryResults If http 204 (No Content) is returned by server see https://dicom.nema.org/medical/dicom/current/output/chtml/ part18/sect_8.3.4.4.html DICOMTrolleyError If response is otherwise not as expected

Source code in dicomtrolley/wado_rs.py
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
@staticmethod
def check_for_response_errors(response):
    """Raise exceptions if this response is not a valid WADO-RS response.

    Parameters
    ----------
    response: Response
        requests.Response as returned from a wado-rs call

    Raises
    ------
    NoQueryResults
        If http 204 (No Content) is returned by server
        see https://dicom.nema.org/medical/dicom/current/output/chtml/
        part18/sect_8.3.4.4.html
    DICOMTrolleyError
        If response is otherwise not as expected
    """
    if response.status_code == 204:
        raise NoQueryResultsError("Server returned http 204 (No Content)")
    elif response.status_code != 200:
        raise DICOMTrolleyError(
            f"Calling {response.url} failed ({response.status_code} - "
            f"{response.reason})\n"
            f"response content was {str(response.content[:300])}"
        )

datasets(objects)

Retrieve each instance

Returns

Iterator[Dataset, None, None]

Raises

DICOMTrolleyError If getting does not work for some reason

Source code in dicomtrolley/wado_rs.py
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
def datasets(self, objects: Sequence[DICOMDownloadable]):
    """Retrieve each instance

    Returns
    -------
    Iterator[Dataset, None, None]

    Raises
    ------
    DICOMTrolleyError
        If getting does not work for some reason
    """
    logger.debug("Getting datasets")
    if isinstance(objects, DICOMDownloadable):
        objects = [objects]  # handle passing single object instead of list

    return chain.from_iterable(
        self.download_iterator(obj) for obj in objects
    )

download_iterator(downloadable)

Perform a wado RS request and iterate over the returned datasets.

Returns

Iterator[Dataset, None, None] All datasets included in the response

Source code in dicomtrolley/wado_rs.py
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
def download_iterator(self, downloadable: DICOMDownloadable):
    """Perform a wado RS request and iterate over the returned datasets.

    Returns
    -------
    Iterator[Dataset, None, None]
        All datasets included in the response
    """
    uri = self.wado_rs_instance_uri(downloadable.reference())
    logger.debug(f"Calling {uri}")
    response = self.session.get(
        url=uri,
        stream=True,
    )
    response_parsed = json.loads(response.text)
    logger.debug(
        f"Received {len(response_parsed)} metadata results. Parsing."
    )

    return iter(
        Dataset.from_json(x, bulk_data_uri_handler=self.bulk_data_reader)
        for x in response_parsed
    )

wado_rs_instance_uri(reference)

WADO-RS URI to request all instances contained in referenced object

Source code in dicomtrolley/wado_rs.py
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
def wado_rs_instance_uri(self, reference: DICOMObjectReference):
    """WADO-RS URI to request all instances contained in referenced object"""
    uri = self.url.rstrip(
        "/"
    )  # self.url might or might not have trailing /
    if isinstance(reference, StudyReference):
        # Note: study-level /metadata is not in the DICOM standard, but some
        # VNA systems still implement it
        return f"{uri}/studies/{reference.study_uid}/metadata"
    elif isinstance(reference, SeriesReference):
        return (
            f"{uri}/studies/{reference.study_uid}/series"
            f"/{reference.series_uid}/metadata"
        )
    elif isinstance(reference, InstanceReference):
        return (
            f"{uri}/studies/{reference.study_uid}/series"
            f"/{reference.series_uid}/instances/{reference.instance_uid}/metadata"
        )
    else:
        raise ValueError(
            f"StudyReference or InstanceReference expected. "
            f"Found {type(reference)}"
        )